Реферат по предмету "Информатика"


VB MS Access VC Delphi Builder C принципытехнология алгоритмы программирования

programmer@newmail.ru

Далееследует «текст», который любойуважающий себяпрограммистдолжен прочестьхотя бы одинраз. (Это нашесубъективноемнение)
Введение
Программированиепод Windows всегдабыло нелегкойзадачей. Интерфейсприкладногопрограммирования(Application ProgrammingInterface)Windowsпредоставляетв распоряжениепрограммистанабор мощных, но не всегдабезопасныхинструментовдля разработкиприложений.Можно сравнитьего с бульдозером, при помощикоторого удаетсядобитьсяпоразительныхрезультатов, но без соответствующихнавыков иосторожности, скорее всего, дело закончитсятолько разрушениямии убытками.
Этакартина измениласьс появлениемVisual Basic.Используявизуальныйинтерфейс,Visual Basicпозволяетбыстро и легкоразрабатыватьзаконченныеприложения.При помощиVisual Basicможно разрабатыватьи тестироватьсложные приложениябез прямогоиспользованияфункций API. Избавляяпрограммистаот проблем сAPI, Visual Basicпозволяетсконцентрироватьсяна деталяхприложения.
ХотяVisual Basic иоблегчаетразработкупользовательскогоинтерфейса, задача написаниякода для реакциина входныевоздействия, обработки их, и представлениярезультатовложится наплечи программиста.Здесь начинаетсяприменениеалгоритмов.
Алгоритмыпредставляютсобой формальныеинструкциидля выполнениясложных задачна компьютере.Например, алгоритмсортировкиможет определять, как найти конкретнуюзапись в базеиз 10 миллионовзаписей. Взависимостиот классаиспользуемыхалгоритмовискомые данныемогут бытьнайдены засекунды, часыили вообще ненайдены.
В этомматериалеобсуждаютсяалгоритмы наVisual Basic исодержитсябольшое числомощных алгоритмов, полностьюнаписанныхна этом языке.В ней такжеанализируютсяметоды обращениясо структурамиданных, такими, как списки, стеки, очередии деревья, иалгоритмы длявыполнениятипичных задач, таких как сортировка, поиск и хэширование.
Длятого чтобыуспешно применятьэти алгоритмы, недостаточноих просто скопироватьв свою программу.Необходимокроме этогопонимать, какразличныеалгоритмы ведутсебя в разныхситуациях, чтов конечномитоге и будетопределятьвыбор наиболееподходящегоалгоритма.
В этомматериалеповедениеалгоритмовв типичном инаихудшемслучаях описанодоступнымязыком. Этопозволит понять, чего вы вправеожидать от тогоили иного алгоритмаи распознать, в каких условияхвстречаетсянаихудшийслучай, и всоответствиис этим переписатьили поменятьалгоритм. Дажесамый лучшийалгоритм непоможет в решениизадачи, еслиприменять егонеправильно.

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

Всеалгоритмы такжепредставленыв виде исходныхтекстов наVisual Basic, которые выможете использоватьв своих программахбез каких либоизменений. Онидемонстрируютиспользованиеалгоритмовв программах, а также важныехарактерныеособенностиработы самихалгоритмов.
Чтодают вам этизнания
Послеознакомленияс данным материаломи примерамивы получите:
Понятие об алгоритмах. После прочтения данного материала и выполнения примеров программ, вы сможете применять сложные алгоритмы в своих проектах на Visual Basic и критически оценивать другие алгоритмы, написанные вами или кем либо еще.
Большую подборку исходных текстов, которые вы сможете легко добавить к вашим программам. Используя код, содержащийся в примерах, вы сможете легко добавлять мощные алгоритмы к вашим приложениям.
Готовые примеры программ дадут вам возможность протестировать алгоритмы. Вы можете использовать эти примеры и модифицировать их для углубленного изучения алгоритмов и понимания их работы, или использовать их как основу для разработки собственных приложений.Целеваяаудитория
В этомматериалеобсуждаютсяуглубленныевопросы программированияна Visual Basic.Они не предназначенадля обученияпрограммированиюна этом языке.Если вы хорошоразбираетесьв основахпрограммированияна Visual Basic, вы сможетесконцентрироватьвнимание наалгоритмахвместо того, чтобы застреватьна деталяхязыка.
В этомматериалеизложены важныеконцепциипрограммирования, которые могутбыть с успехомприменены длярешения новыхзадач. Приведенныеалгоритмыиспользуютмощные программныеметоды, такиекак рекурсия, разбиение начасти, динамическоераспределениепамяти и сетевыеструктурыданных, которыевы можете применятьдля решениясвоих конкретныхзадач.
Дажеесли вы еще неовладели вполной мерепрограммированиемна Visual Basic, вы сможетескомпилироватьпримеры программи сравнитьпроизводительностьразличныхалгоритмов.Более того, высможете выбратьудовлетворяющиевашим требованиямалгоритмы идобавить ихк вашим проектамна Visual Basic.
Совместимостьс разными версиямиVisualBasic
Выборнаилучшегоалгоритмаопределяетсяне особенностямиверсии языкапрограммирования, а фундаментальнымипринципамипрограммирования.

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

Некоторыеновые понятия, такие как ссылкина объекты, классы и коллекции, которые быливпервые введеныв 4-й версии VisualBasic, облегчаютпонимание, разработкуи отладку некоторыхалгоритмов.Классы могутзаключатьнекоторыеалгоритмы вхорошо продуманныхмодулях, которыелегко вставитьв программу.Хотя для того, чтобы применятьэти алгоритмы, необязательноразбиратьсяв новых понятияхязыка, эти новыевозможностипредоставляютслишком большиепреимущества, чтобы ими можнобыло пренебречь.
Поэтомупримеры алгоритмовв этом материаленаписаны дляиспользованияв 4-й и 5-й версияхVisual. Если выоткроете ихв 5-й версии VisualBasic, средаразработкипредложит вамсохранить ихв формате 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 рассматриваетсяблизкая к сортировкетема. Послевыполнениясортировкисписка, программеможет понадобитьсянайти элементыв нем. В этойглаве сравниваетсянескольконаиболее эффективныхметодов поискаэлементов всортированныхсписках.
--PAGE_BREAK--
=====69

PrivateSub AtoB(ByVal I As Integer, ByVal J As Integer, X As Integer)
Dimtmp As Integer

IfI
tmp= I
I= J
J= tmp
EndIf
I= I + 1
X= I * (I — 1) / 2 + J
EndSub

ПроцедурапреобразованияBtoAдолжна вычитатьиз I единицуперед возвратомзначения.

PrivateSub 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
EndSub

ПрограммаTriang2аналогичнапрограммеTriang, но она используетдля работы сдиагональнымиэлементамив массиве A этиновые функции.ПрограммаTriangC2аналогичнапрограммеTriangC, но используеткласс TriangularArray, который включаетдиагональныеэлементы.Нерегулярныемассивы
В некоторыхпрограммахнужны массивынестандартногоразмера и формы.Двумерныймассив можетсодержать шестьэлементов впервом ряду, три — во втором, четыре — втретьем, и т.д.Это можетпонадобиться, например, длясохраненияряда многоугольников, каждый из которыхсостоит изразного числаточек. Массивбудет при этомвыглядеть, какна рис. 4.3.
Массивыв Visual Basicне могут иметьтакие неровныекрая. Можнобыло бы использоватьмассив, достаточнобольшой длятого, чтобы внем могли поместитьсявсе строки, нопри этом в такоммассиве былобы множествонеиспользуемыхячеек. Например, массив на рис.4.3 мог бы бытьобъявлен припомощи оператораDimPolygons(1To3, 1 To6), и приэтом четыреячейки останутсянеиспользованными.
Существуетнесколькоспособовпредставлениянерегулярныхмассивов.

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

=====70
Прямаязвезда
Одиниз способовизбежать потерьпамяти заключаетсяв том, чтобыупаковатьданные в одномерноммассиве B. В отличиеот треугольныхмассивов, длянерегулярныхмассивов нельзязаписать формулыдля определениясоответствияэлементов вразных массивах.Чтобы справитьсяс этой задачей, можно создатьеще один массивA со смещениямидля каждойстроки в одномерноммассиве B.
Дляупрощенияопределенияв массиве B положенияточек, соответствующихкаждой строке, в конец массиваA можно добавитьсигнальнуюметку, котораяуказывает наточку сразуза последнимэлементом вмассиве B. Тогдаточки, образующиемногоугольникI, занимают вмассиве B позициис A(I) до A(I+1)-1. Например, программа можетперечислитьэлементы, образующиестроку I, используяследующий код:

ForJ = A(I) To A(I + 1) — 1
‘Внести в списокэлемент I.
:
NextJ

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

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

=====71

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

На рис.4.6 показанопредставлениев виде прямойзвезды с рис.4.4 после добавленияодной точкик первомумногоугольнику.Элементы, которыебыли изменены, закрашены серымцветом. Каквидно из рисунка, почти все элементыв обоих массивахбыли изменены.Нерегулярныесвязные списки
Другимметодом созданиянерегулярныхмассивов являетсяиспользованиесвязных списков.Каждая ячейкасодержит указательна следующуюячейку на томже уровне иерархии, и указательна список ячеекна более низкомуровне иерархии.Например, ячейкамногоугольникаможет содержатьуказатель наследующиймногоугольники указательна ячейку, содержащуюкоординатыпервой точки.
Следующийкод приводитопределенияпеременныхдля классов, которые можноиспользоватьдля созданиясвязного спискарисунков. Каждыйиз рисунковсодержит связныйсписок многоугольников, каждый из которыхсодержит связныйсписок точек.
ВклассеPictureCell:

DimNextPicture As PictureCell ' Следующийрисунок.
DimFirstPolygon As PolyfonCell ' Первыймногоугольникна этом рисунке.

В классеPolygonCell:

DimNextPolygon As PolygonCell ' Следующиймногоугольник.
DimFirstPoint As PointCell ' Перваяточка в этоммногоугольнике.

В классеPointCell:

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

======72

DimNextPoint As PointCell ' Следующаяточка в этоммногоугольнике.
DimX As Single ' Координатыточки.
DimY 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.
Чтобыпостроитьразреженныймассив в VisualBasic, создайтекласс дляпредставленияэлементовмассива. В этомслучае, каждаяячейка представляетналичие рейсовмежду двумягородами. Дляпредставлениясвязи, классдолжен содержатьпеременныес индексамигородов, которыесвязаны междусобой. Эти индексы, в сущности, дают номерастрок и столбцовячейки. Каждаяячейка такжедолжна содержатьуказатели наследующуюячейку в строкеи столбце.
Следующийкод показываетобъявлениепеременныхв классе ConnectionCell:

PublicFromCity As Integer ' Строкаячейки.
PublicToCity As Integer ' Столбецячейки.
PublicNextInRow As ConnectionCell
PublicNextInCol As ConnectionCell

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

PrivateSub PrintRow(I As Integer)
Dimcell As ConnectionCell

SetCell = RowHead(I).Next ' Первыйэлементданных.
DoWhile Not (cell Is Nothing)
PrintFormat$(cell.FromCity) & " -> " &Format$(cell.ToCity)
Setcell = cell.NextInRow
Loop
EndSub

====74

@Рис.4.9. Разреженнаяматрица смежности
Индексированиемассива
Нормальноеиндексированиемассива типаA(I, J) не будет работатьс такими структурами.Можно облегчитьиндексирование, написав процедуры, которые извлекаюти устанавливаютзначения элементовмассива. Еслимассив представляетматрицу, могуттакже понадобитьсяпроцедуры длясложения, умножения, и других матричныхопераций.
Специальноезначение NoValueпредставляетпустой элементмассива. Процедура, которая извлекаетэлементы массива, должна возвращатьзначение NoValueпри попыткеполучить значениеэлемента, несодержащегосяв массиве.Аналогично, процедура, которая устанавливаетзначения элементов, должна удалятьячейку из массива, если ее значениеустановленов NoValue.
ЗначениеNoValueдолжно выбиратьсяв зависимостиот природыданных приложения.Для матрицысмежностиавиалиниипустые ячейкимогут иметьзначение False.При этом значениеA(I,J)может устанавливатьсяравным True, если существуетрейс междугородами Iи J.
КлассSparseArrayопределяетпроцедуру getдля свойстваValueдля возвращениязначения элементав массиве. Процедураначинает спервой ячейкив указаннойстроке и затемперемещаетсяпо связномусписку ячеекстроки. Кактолько найдетсяячейка с нужнымномером столбца, это и будетискомая ячейка.Так как ячейкив списке строкирасположеныпо порядку, процедура можетостановиться, если найдетсяячейка, номерстолбца которойбольше искомого.

=====75

PropertyGet Value(t As Integer, c As Integer) As Variant
Dimcell As SparseArrayCell
Value= NoValue ' Предположим, что мы не найдемэлемент.
Ifr
r> NumRows Or c > NumCols _
ThenExit Property

Setcell = RowHead(r).NextInRow ' Пропуститьметку.
Do
Ifcell Is Nothing Then Exit Property ' Ненайден.
Ifcell.Col > c Then Exit Property ' Ненайден.
Ifcell.Col = c Then Exit Do ' Найден.
Setcell = cell.NextInRow
Loop
Value= cell. Data
EndProperty

Процедураletсвойстваvalueприсваиваетячейкеновоезначение.Если новоезначение равноNoValue, процедуравызывает дляудаления элементаиз массива. Впротивномслучае, онаищет требуемоеположениеэлемента внужной строке.Если элементуже существует, процедураобновляет егозначение. Иначе, она создаетновый элементи добавляетего к спискустроки. Затемона добавляетновый элементв правильноеположение всоответствующемсписке столбцов.

PropertyLet Value (r As Integer, c As Integer, new_value As Variant)
Dimi As Integer
Dimfound_it As Boolean
Dimcell As SparseArrayCell
Dimnxt As SparseArrayCell
Dimnew_cell As SparseArrayCell

'Если value = MoValue, удалитьэлемент измассива.
Ifnew_value = NoValue Then
RemoveEntryr, c
ExitProperty
EndIf

'Если нужно, добавить строки.
Ifr > NumRows Then
ReDimPreserve RowHead(1 To r)

'Инициализироватьметку для каждойновой строки.
Fori = NumRows + 1 To r
SetRowHead(i) = New SparseArrayCell
Nexti
EndIf

'Если нужно, добавить столбцы.
Ifc > NumCols Then
ReDimPreserve ColHead(1 To c)

'Инициализироватьметку для каждойновой строки.
Fori = NumCols + 1 To c
SetColHead(i) = New SparseArrayCell
Nexti
NumCols= c
EndIf

'Попытка найтиэлемент.
Setcell = RowHead(r)
Setnxt = cell.NextInRow
Do
Ifnxt Is Nothing Then Exit Do
Ifnxt.Col >= c Then Exit Do
Setcell = nxt
Setnxt = cell.NextInRow
Loop

'Проверка, найденли элемент.
Ifnxt Is Nothing Then
found_it= False
Else
found_it= (nxt.Col = c)
EndIf

'Если элементне найден, создатьего.
IfNot found_it Then
Setnew_cell = New SparseArrayCell

'Поместитьэлемент в списокстроки.
Setnew_cell.NextInRow = nxt
Setcell.NextInRow = new_cell

'Поместитьэлемент в списокстолбца.
Setcell = ColHead(c)
Setnxt = cell.NextInCol
Do
Ifnxt Is Nothing Then Exit Do
Ifnxt.Col >= c Then Exit Do
Setcell = nxt
Setnxt = cell.NextInRow
Loop

Setnew_cell.NextInCol = nxt
Setcell.NextInCol = new_cell
new_cell.Row= r
new_cell.Col= c

'Поместим значениев элемент nxt.
Setnxt = new_cell
EndIf

'Установимзначение.
nxt.Data= new_value
EndProperty

ПрограммаSparse, показаннаяна рис. 4.10, используетклассы SparseArrayи SparseArrayCellдля работы сразреженныммассивом. Используяпрограмму, можно устанавливатьи извлекатьэлементы массива.В этой программезначение NoValueравно нулю, поэтому есливы установитезначение элементаравным нулю, программаудалит этотэлемент измассива.Оченьразреженныемассивы
Некоторыемассивы содержаттак мало непустыхэлементов, чтомногие строкии столбцы полностьюпусты. В этомслучае, лучшехранить заголовкистрок и столбцовв связных списках, а не в массивах.Это позволяетпрограммеполностьюпропускатьпустые строкии столбцы. Заголовкистроки и столбцовуказывают насвязные спискиэлементов строки столбцов. Нарис. 4.11 показанмассив 100 на 100, который содержитвсего 7 непустыхэлементов.

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

=====76-78

@Рис.4.11. Очень разреженныймассив

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

PublicNumber As Integer ' Номерстрокиили столбца.
PublicSentinel As SparseArrayCell ' Меткадля строкиили
'столбца.
PublicNextHeader As HeaderCell ' Следующаястрокаили
'столбец.

Например, чтобы обратитьсяк строке I, нужно вначалепросмотретьсвязный списокзаголовковHeaderCellsстрок, пока ненайдется заголовок, соответствующийстроке I.Затем продолжаетсяработа со строкойI.

PrivateSub PrintRow(r As Integer)
Dimrow As HeaderCell
Dimcell As SparseArrayCell

'Найти правильныйзаголовокстроки.
Setrow = RowHead. NextHeader ' Списокпервой строки.
Do
Ifrow Is Nothing Then Exit Sub ' Такойстрокинет.
Ifrow.Number > r Then Exit Sub ' Такойстрокинет.
Ifrow.Number = r Then Exit Do ' Строканайдена.
Setrow = row.NextHeader
Loop

'Вывести элементыв строке.
Setcell = row.Sentinel. NextInRow ' Первыйэлементв строке.

DoWhile Not (cell Is Nothing)
PrintFormat$(cell.FromCity) & " -> " &Format$(cell.ToCity)
Setcell = cell.NextInRow
Loop
EndSub
Резюме
Некоторыепрограммыиспользуютмассивы, содержащиетолько небольшоечисло значащихэлементов.Использованиеобычных массивовVisual Basicпривело бы кбольшим потерямпамяти. Используятреугольные, нерегулярные, разреженныеи очень разреженныемассивы, выможете создаватьмощные представлениямассивов, которыетребуют намногоменьших объемовпамяти.

=========80
Глава5. Рекурсия
Рекурсия —мощный методпрограммирования, который позволяетразбить задачуна части всеменьшего именьшего размерадо тех пор, покаони не станутнастолько малы, что решениеэтих подзадачсведется кнабору простыхопераций.
Послетого, как выприобрететеопыт применениярекурсии, выбудете обнаруживатьее повсюду.Многие программисты, недавно овладевшиерекурсией, увлекаются, и начинаютприменять еев ситуациях, когда она являетсяненужной, аиногда и вредной.
Впервых разделахэтой главыобсуждаетсявычислениефакториалов, чисел Фибоначчи, и наибольшегообщего делителя.Все эти алгоритмыявляются примерамиплохого использованиярекурсии —нерекурсивныеверсии этихалгоритмовнамного эффективнее.Эти примерыинтересны инаглядны, поэтомуимеет смыслобсудить их.
Затем, в главе рассматриваетсянесколькопримеров, вкоторых применениерекурсии болееуместно. Алгоритмыпостроениякривых Гильбертаи Серпинскогоиспользуютрекурсию правильнои эффективно.
Впоследнихразделах этойглавы объясняется, почему реализациюалгоритмоввычисленияфакториалов, чисел Фибоначчи, и наибольшегообщего делителялучше осуществлятьбез применениярекурсии. Вэтих параграфахобъясняетсятакже, когдаследует избегатьрекурсии, иприводятсяспособы устранениярекурсии, еслиэто необходимо.Что такоерекурсия?
Рекурсияпроисходит, если функцияили подпрограммавызывает самасебя. Прямаярекурсия (directrecursion) выглядитпримерно так:
    продолжение
--PAGE_BREAK--
FunctionFactorial(num As Long) As Long
Factorial= num * Factorial(num — 1)
EndFunction

Вслучае косвеннойрекурсии(indirect recursion)рекурсивнаяпроцедуравызывает другуюпроцедуру, которая, в своюочередь, вызываетпервую:

PrivateSub Ping(num As Integer)
Pong(num- 1)
EndSub

PrivateSub Pong(num As Integer)
Ping(num/ 2)
EndSub

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

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

PrivateSub DrawTree()
Нарисовать«ствол»
Нарисоватьдерево меньшегоразмера, повернутоена -45 градусов
Нарисоватьдерево меньшегоразмера, повернутоена 45 градусов
EndSub

Хотярекурсия иможет упроститьпониманиенекоторыхпроблем, людиобычно не мыслятрекурсивно.Они обычностремятсяразбить сложныезадачи на задачименьшего объема, которые могутбыть выполненыпоследовательноодна за другойдо полногозавершения.Например, чтобыпокраситьизгородь, можноначать с еелевого краяи продолжатьдвигатьсявправо до завершения.Вероятно, вовремя выполненияподобной задачивы не думаетео возможностирекурсивнойокраски —вначале левойполовины изгороди, а затем рекурсивно —правой.
Длятого чтобыдумать рекурсивно, нужно разбитьзадачу на подзадачи, которые затемможно разбитьна подзадачименьшего размера.В какой томомент подзадачистановятсянастолькопростыми, чтомогут бытьвыполненынепосредственно.Когда завершитсявыполнениеподзадач, большиеподзадачи, которые из нихсоставлены, также будутвыполнены.Исходная задачаокажется выполнена, когда будутвсе выполненыобразующиеее подзадачи.Рекурсивноевычислениефакториалов
Факториалчисла 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. Значения функциифакториала

Легконаписать наоснове этогоопределениярекурсивнуюфункцию:

PublicFunction Factorial(num As Integer) As Integer
Ifnum
Factorial= 1
Else
Factorial= num * Factorial(num — 1)
EndIf
EndFunction

Вначалеэта функцияпроверяет, чточисло меньшеили равно 0.Факториал длячисел меньшенуля не определен, но это условиепроверяетсядля подстраховки.Если бы функцияпроверялатолько условиеравенства числанулю, то дляотрицательныхчисел рекурсиябыла бы бесконечной.
Есливходное значениеменьше илиравно 0, функциявозвращаетзначение 1. Востальныхслучаях, значениефункции равнопроизведениювходного значенияна факториалот входногозначения, уменьшенногона единицу.
То, что эта рекурсивнаяфункция в концеконцов остановится, гарантируетсядвумя фактами.Во первых, прикаждом последующемвызове, значениепараметра numуменьшаетсяна единицу.Во вторых, значение numограниченоснизу нулем.Когда numстановитсяравным 0, функцияостанавливаетрекурсию. Условие, например, вданном случаеусловие numусловиемостановкирекурсии (basecase или stoppingcase).
Прикаждом вызовеподпрограммы, система сохраняетряд параметровв системномстеке, какописывалосьв 3 главе. Таккак этот стекиграет важнуюроль, иногдаего называютпросто стеком.Если рекурсивнаяфункция вызоветсебя слишкоммного раз, онаможет исчерпатьстековое пространствои аварийнозавершитьработу с ошибкой«Out ofstack space».
Числораз, котороефункция можетвызвать самасебя до того, как используетвсе стековоепространство, зависит отобъема установленнойна компьютерепамяти и количестваданных, помещаемыхпрограммойв стек. В одномиз тестов, программаисчерпаластековое пространствопосле 452 рекурсивныхвызовов. Послеизменениярекурсивнойфункции такимобразом, чтобыона определяла10 локальныхпеременныхпри каждомвызове, программамогла вызватьсебя только271 раз.Анализвремени выполненияпрограммы
Функциифакториалатребуетсяединственныйаргумент: число, факториал откоторого требуетсявычислить.Анализ вычислительнойсложностиалгоритмаобычно исследуетзависимостьвремени выполненияпрограммы какфункции отразмерности(size) задачиили числа входныхзначений (numberof inputs).Поскольку вданном случаевходное значениевсего одно, такие расчетымогли бы показатьсянемного странными.

========83

Поэтому, алгоритмы сединственнымвходным параметромобычно оцениваютсячерез числобитов, необходимыхдля хранениявходного значения, а не число входныхзначений. Внекоторомсмысле, это иесть размервхода, так какстолько биттребуется длятого, чтобызаписать входноезначение. Темне менее, этоне очень наглядныйспособ представленияэтой задачи.Кроме того, теоретическикомпьютер могбы записатьвходное значениеN в log2(N) бит, но вдействительностивероятнее всегоN занимаетфиксированноечисло битов.Например, всечисла форматаlongзанимают 32 бита.
Поэтомув этой главеалгоритмы этоготипа анализируютсяна основе значениявхода, а не егоразмерности.Если вы хотитепереписатьрезультатыв терминахразмерностивхода, вы можетеэто сделать, воспользовавшисьтем, что N=2M, гдеМ — число битов, необходимоедля записи N.Если времявыполненияалгоритмапорядка O(N2) втерминах входногозначения N, тооно составитпорядка O((22M)2)= O(22*M) = O((22)M) = O(4M)в терминахразмерностивхода M.
Функциипорядка O(N) растутдовольно медленно, поэтому можноожидать отэтого алгоритмахорошей производительности.Так оно и есть.Эта функцияприводит кпроблемамтолько припереполнениистека послемножестварекурсивныхвызовов, иликогда значениеN! становитсяслишком большими не помещаетсяв формат целогочисла, вызываяошибку переполнения.
Таккак N! растеточень быстро, переполнениенаступаетраньше, еслитолько стекне используетсяинтенсивнодля другихцелей. Прииспользованииданных целоготипа, переполнениенаступает для8!, поскольку8! = 40.320, что больше, чем наибольшеецелое число32.767. Для того чтобыпрограмма моглавычислятьприближенныезначения факториалабольших чисел, можно изменитьфункцию, используявместо целыхчисел значениятипа double.Тогда максимальноечисло, котороесможет вычислитьалгоритм, будетравно 170! = 7,257E+306.
ПрограммаFactoдемонстрируетрекурсивнуюфункцию факториала.Введите значениеи нажмите накнопку Go, чтобы вычислитьего факториал.Рекурсивноевычислениенаибольшегообщего делителя
Наибольшимобщим делителем(greatest commondivisor, GCD) двухчисел называетсянаибольшеецелое, на котороеделятся двачисла без остатка.Например, наибольшийобщий делительчисел 12 и 9 равен3. Два числаназываютсявзаимно простыми(relatively prime), если их наибольшийобщий делительравен 1.
МатематикЭйлер, жившийв восемнадцатомвеке, обнаружилинтересныйфакт:

ЕслиA нацело делитсяна B, то GCD(A, B) = A.
ИначеGCD(A, B) = GCD(B Mod A, A).

Этотфакт можноиспользоватьдля быстроговычислениянаибольшегообщего делителя.Например:

GCD(9,12) = GCD(12 Mod 9, 9)
=GCD(3, 9)
=3

========84

Накаждом шагечисла становятсявсе меньше, таккак 1
ОткрытиеЭйлера закономернымобразом приводитк рекурсивномуалгоритмувычислениянаибольшегообщего делителя:

publicFunction GCD(A As Integer, B As Integer) As Integer
IfB Mod A = 0 Then ' Делитсяли B на A нацело?
GCD= A ' Да. Процедуразавершена.
Else
GCD= GCD(B Mod A, A) ' Нет.Рекурсия.
EndIf
EndFunction
Анализвремени выполненияпрограммы
Чтобыпроанализироватьвремя выполненияэтого алгоритма, необходимоопределить, насколькобыстро убываетпеременнаяA.Так как функцияостанавливается, когда Aдоходит дозначения 1, тоскорость уменьшенияAдает верхнююграницу оценкивремени выполненияалгоритма.Оказывается, при каждомвтором вызовефункции GCD, параметр Aуменьшается, по крайнеймере, в 2 раза.
Допустим,A
Предположимобратное. Допустим,BModA> A/ 2. Первымрекурсивнымвызовом функцииGCDбудет GCD(BModA,A).
Подстановкав функцию значенияBModAи Aвместо Aи Bдает следующийрекурсивныйвызов GCD(BModA,A).
Номы предположили, что BModA> A/ 2. ТогдаBModAразделитсяна Aтолько одинраз, с остаткомA– (BModA).Так как B Mod Aбольше, чем A/ 2, то A– (BModA)должно бытьменьше, чем 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)).Это не обязательноозначает, чтоэтими постояннымиможно полностьюпренебречьпри реализацииалгоритма.Алгоритм, которыйуменьшаетразмер задачипри каждом шагев 10 раз, вероятно, будет быстрее, чем алгоритм, который уменьшаетразмер задачивдвое черезкаждые 5 шагов.Тем не менее, оба эти алгоритмаимеют времявыполненияпорядка O(log(N)).
Алгоритмыпорядка O(log(N))обычно выполняютсяочень быстро, и алгоритмнахождениянаибольшегообщего делителяне являетсяисключениемиз этого правила.Например, чтобынайти, что наибольшийобщий делительчисел 1.736.751.235 и2.135.723.523 равен 71, функциявызываетсявсего 17 раз.Фактически, алгоритм практическимгновенновычисляетзначения, непревышающиемаксимальногозначения числав формате long —2.147.483.647. ФункцияVisual Basic Modне может оперироватьзначениями, большими этого, поэтому этопрактическийпредел дляданной реализацииалгоритма.
ПрограммаGCDиспользуетэтот алгоритмдля рекурсивноговычислениянаибольшегообщего делителя.Введите значениядля A и B, затемнажмите накнопку Go, и программавычислит наибольшийобщий делительэтих двух чисел.Рекурсивноевычислениечисел Фибоначчи
Можнорекурсивноопределитьчисла Фибоначчи(Fibonacci numbers)при помощиуравнений:

Fib(0)= 0
Fib(1)= 1
Fib(N)= Fib(N — 1) + Fib(N — 2) дляN > 1.

Третьеуравнениерекурсивнодважды вызываетфункцию Fib, один раз с входнымзначением N-1, а другой — созначением N-2.Это определяетнеобходимость2 условий остановкирекурсии: Fib(0)=0и Fib(1)=1.Если задатьтолько одноиз них, рекурсияможет оказатьсябесконечной.Например, еслизадать толькоFib(0)=0, то значениеFib(2)могло бы вычислятьсяследующимобразом:

Fib(2) =Fib(1) + Fib(0)
=[Fib(0) + Fib(-1)] + 0
=0 + [Fib(-2) + Fib(-3)]
=[Fib(-3) + Fib(-4)] + [Fib(-4) + Fib(-5)]
Ит.д.

Этоопределениечисел Фибоначчилегко преобразоватьв рекурсивнуюфункцию:

PublicFunction Fib(num As Integer) As Integer
Ifnum
Fib= num
Else
Fib= Fib(num – 1) + Fib(num — 2)
EndIf
EndFunction

=========86
Анализвремени выполненияпрограммы
Анализэтого алгоритмадостаточносложен. Во первых, определим, сколько развыполняетсяодно из условийостановки num
ЕслиN > 1, то функциярекурсивновычисляетFib(N-1)и Fib(N-2), и завершаетработу. Припервом вызовефункции, условиеостановки невыполняется —оно достигаетсятолько в следующих, рекурсивныхвызовах. Полноечисло выполненияусловия остановкидля входногозначения N, складываетсяиз числа раз, которое оновыполняетсядля значенияN-1и числа раз, которое оновыполнялосьдля значенияN-2.Все это можнозаписать так:

G(0)= 1
G(1)= 1
G(N)= G(N — 1) + G(N — 2) для N > 1.

Эторекурсивноеопределениеочень похожена определениечисел Фибоначчи.В табл. 5.2 приведенынекоторыезначения функцийG(N)и Fib(N).Легко увидеть, что G(N)= Fib(N+1).
Теперьрассмотрим, сколько разалгоритм достигаетрекурсивногошага. Если N1, функция достигаетэтого шага 1раз и затемрекурсивновычисляетFib(n-1)и Fib(N-2).Пусть H(N) —число раз, котороеалгоритм достигаетрекурсивногошага для входаN.Тогда H(N)=1+H(N-1)+H(N-2).Уравнения, определяющиеH(N):

H(0)= 0
H(1)= 0
H(N)= 1 + H(N — 1) + H(N — 2) для N > 1.

В табл.5.3 показанынекоторыезначения дляфункций Fib(N)и H(N).Можно увидеть, что H(N)=Fib(N+1)-1.

@Таблица5.2. Значения чиселФибоначчи ифункции G(N)

======87

@Таблица5.3. Значения чиселФибоначчи ифункции H(N)

Объединяярезультатыдля G(N)и H(N), получаем полноевремя выполнениядля алгоритма:

Времявыполнения =G(N) + H(N)
=Fib(N + 1) + Fib(N + 1) — 1
=2 * Fib(N + 1) — 1

ПосколькуFib(N+ 1) >= Fib(N)для всех значенийN, то:

Времявыполнения >=2 * Fib(N) — 1

С точностьюдо порядка этосоставит O(Fib(N)).Интересно, чтоэта функцияне толькорекурсивная, но она такжеиспользуетсядля оценкивремени еевыполнения.
Чтобыпомочь вампредставитьскорость ростафункции Фибоначчи, можно показать, что Fib(M)>M-2где  —константа, примерно равная1,6. Это означает, что время выполненияне меньше, чемзначениеэкспоненциальнойфункции O(M).Как и другиеэкспоненциальныефункции, этафункция растетбыстрее, чемполиномиальныефункции, номедленнее, чемфункция факториала.
Посколькувремя выполнениярастет оченьбыстро, этоталгоритм довольномедленно выполняетсядля большихвходных значений.Фактически, настолькомедленно, чтона практикепочти невозможновычислитьзначения функцииFib(N)для N, которые намногобольше 30. В табл.5.4 показано времявыполнениядля этого алгоритмана компьютерес процессоромPentium с тактовойчастотой 90 МГцпри разныхвходных значениях.
ПрограммаFiboиспользуетэтот рекурсивныйалгоритм длявычислениячисел Фибоначчи.Введите целоечисло и нажмитена кнопку Goдля вычислениячисел Фибоначчи.Начните с небольшихчисел, пока неоцените, насколькобыстро вашкомпьютер можетвыполнять этивычисления.Рекурсивноепостроениекривых Гильберта
КривыеГильберта(Hilbert curves) —это самоподобные(self similar)кривые, которыеобычно определяютсяпри помощирекурсии. Нарис. 5.2. показаныкривые Гильбертас 1, 2 или 3 порядка.

@Таблица5.4. Время выполненияпрограммыFibonacci

=====88

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

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

Line-Step (Length,0)
Line-Step (0,Length)
Line-Step (-Length,0)
=1, функция>    продолжение
--PAGE_BREAK--
Предполагается, что рисованиеначинаетсяс верхнеголевого углаобласти и чтоLength —это заданнаядлина каждогоотрезка линий.
Можнонабросатьчерновик метода, рисующегокривые Гильбертаболее высокихпорядков:

PrivateSub Hilbert(Depth As Integer)
IfDepth = 1 Then
Нарисоватькривую Гильберта1 порядка
Else
Нарисоватьи соединить4 кривые порядка(Depth — 1)
EndIf
EndSub

====89

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

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

PrivateSub Hilbert(depth As Integer, Dx As Single, Dy As Single)
Ifdepth > 1 Then Hilbert depth — 1, Dy, Dx
HilbertPicture.Line-Step(Dx, Dy)
Ifdepth > 1 Then Hilbert depth — 1, Dx, Dy
HilbertPicture.Line-Step(Dy, Dx)
Ifdepth > 1 Then Hilbert depth — 1, Dx, Dy
HilbertPicture.Line-Step(-Dx, -Dy)
Ifdepth > 1 Then Hilbert depth — 1, -Dy, -Dx
EndSub
Анализвремени выполненияпрограммы
Чтобыпроанализироватьвремя выполненияэтой процедуры, вы можете определитьчисло вызововпроцедурыHilbert.При каждойрекурсии онавызывает себячетыре раза.Если T(N) —это число вызововпроцедуры, когда она вызываетсяс глубинойрекурсии N, то:

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

Еслираскрыть определениеT(N), получим:

T(N) =1 + 4 * T(N — 1)
=1 + 4 *(1 + 4 * T(N — 2))
=1 + 4 + 16 * T(N — 2)
=1 + 4 + 16 * (1 + 4 * T(N — 3))
=1 + 4 + 16 + 64 * T(N — 3)
=...
=40 + 41 + 42 + 43 +… + 4K * T(N — K)

Раскрывэто уравнениедо тех пор, покане будет выполненоусловие остановкирекурсии T(1)=1, получим:

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

Этоуравнение можноупростить, воспользовавшисьсоотношением:

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

Послепреобразования, уравнениеприводитсяк виду:

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

=====90

С точностьюдо постоянных, эта процедуравыполняетсяза время порядкаO(4N). В табл. 5.5 приведенынесколькопервых значенийфункции временивыполнения.Если вы внимательнопосмотритена эти числа, то увидите, чтоони соответствуютрекурсивномуопределению.
Этоталгоритм являетсятипичным примеромрекурсивногоалгоритма, который выполняетсяза время порядкаO(CN), где C — некотораяпостоянная.При каждомвызове подпрограммыHilbert, она увеличиваетразмерностьзадачи в 4 раза.В общем случае, если при каждомвыполнениинекоторогочисла шаговалгоритмаразмер задачиувеличиваетсяне менее, чемв C раз, то времявыполненияалгоритма будетпорядка O(CN).
Этоповедениепротивоположноповедениюалгоритмапоиска наибольшегообщего делителя.Процедура GCDуменьшаетразмерностьзадачи в 2 разапри каждомвтором своемвызове, и поэтомувремя ее выполненияпорядка O(log(N)).Процедурапостроениякривых Гильбертаувеличиваетразмер задачив 4 раза при каждомсвоем вызове, поэтому времяее выполненияпорядка O(4N).
Функция(4N-1)/3 — этоэкспоненциальнаяфункция, котораярастет оченьбыстро. Фактически, она растетнастолькобыстро, что выможете предположить, что это не слишкомэффективныйалгоритм. Вдействительностиработа этогоалгоритмазанимает многовремени, ноесть две причины, по которым этоне так уж и плохо.
Во-первых, ни один алгоритмдля построениякривых Гильбертане может бытьнамного быстрее.Кривые Гильбертасодержат множествоотрезков линий, и любой рисующийих алгоритмбудет требоватьдостаточномного времени.При каждомвызове процедурыHilbert, она рисует трилинии. ПустьL(N) — суммарноечисло линий, из которыхсостоит криваяГильбертапорядка N. ТогдаL(N) = 3 * T(N) = 4N — 1, поэтомуL(N) также порядкаO(4N). Любой алгоритм, рисующий кривыеГильберта, должен вывестиO(4N) линий, выполнивпри этом O(4N) шагов.Существуютдругие алгоритмыпостроениякривых Гильберта, но они занимаютпочти столькоже времени, сколько и этоталгоритм.

@Таблица5.5. Число рекурсивныхвызовов подпрограммыHilbert

=====91

Второйфакт, которыйпоказывает, что этот алгоритмне так уж плох, заключаетсяв том, что кривыеГильберта 9порядка содержаттак много линий, что экран большинствакомпьютерныхмониторов приэтом оказываетсяполностьюзакрашенным.Это неудивительно, так как этакривая содержит262.143 отрезковлиний. Это означает, что вам вероятноникогда непонадобитсявыводить наэкран кривыеГильберта 9 илиболее высокихпорядков. Накаком то порядкевы столкнетесьс ограничениямиязыка VisualBasic и вашегокомпьютера, но, скорее всего, вы еще раньшебудете ограниченымаксимальнымразрешениемэкрана.
ПрограммаHilbert, показаннаяна рис. 5.4, используетэтот рекурсивныйалгоритм длярисованиякривых Гильберта.При выполнениипрограммы незадавайтеслишком большуюглубину рекурсии(больше 6) до техпор, пока вы неопределите, насколькобыстро выполняетсяэта программана вашем компьютере.Рекурсивноепостроениекривых Серпинского
Как икривые Гильберта, кривые Серпинского(Sierpinski curves) —это самоподобныекривые, которыеобычно определяютсярекурсивно.На рис. 5.5 показаныкривые Серпинского1, 2 и 3 порядка.
Алгоритмпостроениякривых Гильбертаиспользуетвсего однуподпрограммудля рисованиякривых. КривыеСерпинскогопроще рисовать, используячетыре отдельныхпроцедуры, которые работаютсовместно. ЭтипроцедурыназываютсяSierpA,SierpB,SierpCи SierpD.Это процедурыс косвеннойрекурсией —каждая процедуравызывает другие, которые затемвызываютпервоначальнуюпроцедуру. Онирисуют верхнюю, левую, нижнююи правую частикривой Серпинского, соответственно.
На рис.5.6 показано, какэти процедурыработают совместно, образуя кривуюСерпинского1 порядка. Подкривыеизображеныстрелками, чтобы показатьнаправление, в котором онирисуются. Отрезки, соединяющиечетыре подкривые, нарисованыпунктирнымилиниями.

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

=====92

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

Каждаяиз четырехосновных кривыхсостоит издиагональногоотрезка, затемвертикальногоили горизонтальногоотрезка, и ещеодного диагональногоотрезка. Еслиглубина рекурсиибольше единицы, каждая из этихкривых разбиваетсяна меньшиечасти. Этоосуществляетсяразбиениемкаждого из двухдиагональныхотрезков надве подкривые.
Например, для разбиениякривой типаA, первый диагональныйотрезок разбиваетсяна кривую типаA, за которойследует криваятипа B. Затемрисуется безизмененийгоризонтальныйотрезок изисходной кривойтипа A. Наконец, второй диагональныйотрезок разбиваетсяна кривую типаD, за которойследует криваятипа A. На рис.5.7 показано, каккривая типаA второго порядкаобразуетсяиз несколькихкривых 1 порядка.Подкривыеизображеныжирными линиями.
На рис.5.8 показано, какполная криваяСерпинского2 порядка образуетсяиз 4 подкривых1 порядка. Каждаяиз подкривыхобведена контурнойлинией.
Можноиспользоватьстрелки и  дляобозначениятипа линий, соединяющихподкривые(тонкие линиина рис. 5.8), тогдаможно будетизобразитьрекурсивныеотношения междучетырьмя типамикривых так, какэто показанона рис. 5.9.

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

=====93

@Рис.5.7. Разбиениекривой типаA

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

PrivateSub SierpA(Depth As Integer, Dist As Single)
IfDepth = 1 Then
Line-Step(-Dist, Dist)
Line-Step(-Dist, 0)
Line-Step(-Dist, -Dist)
Else
SierpADepth — 1, Dist
Line-Step(-Dist, Dist)
SierpBDepth — 1, Dist
Line-Step(-Dist, 0)
SierpDDepth — 1, Dist
Line-Step(-Dist, -Dist)
SierpADepth — 1, Dist
EndIf
EndSub

@Рис.5.8. Кривые Серпинского, образованныеиз меньшихкривых Серпинского

=====94

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

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

SubSierpinski (Depth As Integer, Dist As Single)
SierpBDepth, Dist
Line-Step(Dist, Dist)
SierpCDepth, Dist
Line-Step(Dist, -Dist)
SierpDDepth, Dist
Line-Step(-Dist, -Dist)
SierpADepth, Dist
Line-Step(-Dist, Dist)
EndSub
Анализвремени выполненияпрограммы
Чтобыпроанализироватьвремя выполненияэтого алгоритма, необходимоопределитьчисло вызововдля каждой изчетырех процедуррисованиякривых. ПустьT(N) —число вызововлюбой из четырехосновных подпрограммосновной процедурыSierpinskiпри построениикривой порядкаN.
Еслипорядок кривойравен 1, криваякаждого типарисуется толькоодин раз. Прибавивсюда основнуюпроцедуру, получим T(1)= 5.
Прикаждом рекурсивномвызове, процедуравызывает самусебя или другиепроцедурычетыре раза.Так как этипроцедурыпрактическиодинаковые, то T(N)будет одинаковым, независимоот того, какаяпроцедуравызываетсяпервой. Этообусловленотем, что кривыеСерпинскогосимметричныи содержат однои то же числокривых разныхтипов. Рекурсивныеуравнения дляT(N)выглядят так:

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

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

=====95

Также, как и алгоритмпостроениякривых Гильберта, этот алгоритмвыполняетсяза время порядкаO(4N), но это нетак уж и плохо.Кривая Серпинскогосостоит изO(4N) линий, поэтомуни один алгоритмне может нарисоватькривую Серпинскогобыстрее, чемза время порядкаO(4N).
КривыеСерпинскоготакже полностьюзаполняют экранбольшинствакомпьютеровпри порядкекривой, большемили равном 9.При каком топорядке, большем9, вы столкнетесьс ограничениямиязыка VisualBasic и возможностейвашего компьютера, но, скорее всего, вы еще раньшебудете ограниченыпредельнымразрешениемэкрана.
ПрограммаSierp, показаннаяна рис. 5.10, используетэтот рекурсивныйалгоритм длярисованиякривых Серпинского.При выполнениипрограммы, задавайтевначале небольшуюглубину рекурсии(меньше 6), до техпор, пока вы неопределите, насколькобыстро выполняетсяэта программана вашем компьютере.Опасностирекурсии
Рекурсияможет служитьмощным методомразбиениябольших задачна части, ноона таит в себенесколькоопасностей.В этом разделемы пытаемсяохватить некоторыеиз этих опасностейи объяснить, когда стоити не стоитиспользоватьрекурсию. Впоследующихразделах приводятсяметоды устраненияот рекурсии, когда это необходимо.Бесконечнаярекурсия
Наиболееочевиднаяопасностьрекурсии заключаетсяв бесконечнойрекурсии. Еслинеправильнопостроитьалгоритм, тофункция можетпропуститьусловие остановкирекурсии ивыполнятьсябесконечно.Проще всегосовершить этуошибку, еслипросто забытьо проверкеусловия остановки, как это сделанов следующейошибочнойверсии функциифакториала.Посколькуфункция непроверяет, достигнутоли условиеостановкирекурсии, онабудет бесконечновызывать самасебя.

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

=====96

PrivateFunction BadFactorial(num As Integer) As Integer
BadFactorial= num * BadFactorial (num — 1)
EndFunction

Функциятакже можетвызывать себябесконечно, если условиеостановки непрекращаетвсе возможныепути рекурсии.В следующейошибочнойверсии функциифакториала, функция будетбесконечновызывать себя, если входноезначение —не целое число, или если ономеньше 0. Этизначения неявляются допустимымивходными значениямидля функциифакториала, поэтому в программе, которая используетэту функцию, может потребоватьсяпроверка входныхзначений. Темне менее, будетлучше, еслифункция выполнитэту проверкусама.

PrivateFunction BadFactorial2(num As Double) As Double
Ifnum = 0 Then
BadFactorial2= 1
Else
BadFactorial2= num * BadFactorial2(num-1)
EndIf
EndFunction

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

PrivateFunction BadFib(num As Double) As Double
Ifnum = 0 Then
BadFib= 0
Else
BadFib= BadPib(num — 1) + BadFib (num — 2)
EndIf
EndFunction

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

PrivateFunction BigAdd(N As Double) As Double
IfN
BigAdd= 1
Else
BigAdd= N + BigAdd(N — 1)
EndIf
EndFunction

=====97

ПрограммаBigAddдемонстрируетэтот алгоритм.Проверьте, насколькобольшое входноезначение выможете ввестив этой программедо того, какнаступит переполнениестека на вашемкомпьютере.Потерипамяти
Другаяопасностьрекурсии заключаетсяв потерях памяти.При каждомвызове подпрограммы, система выделяетпамять длялокальныхпеременныхновой процедуры.Во время сложнойпоследовательностирекурсивныхвызовов, значительнаячасть времении памяти компьютерабудет уходитьна выделениеи освобождениепамяти для этихпеременныхво время рекурсии.Даже если этоне приведетк исчерпаниюстековогопространства, время, потраченноена работу спеременными, может бытьзначительным.
Существуетнесколькоспособов уменьшенияэтих накладныхрасходов. Во первых, не следуетиспользоватьбольшого количестваненужных переменных.Даже еслиподпрограммане используетих, Visual Basicвсе равно будетотводить памятьпод эти переменные.Следующаяверсия функцииBigAddеще быстрееприводит кпереполнениюстека, чемпредыдущая.

PrivateFunction BigAdd(N As Double)As Double
DimI1 As Integer
DimI2 As Integer
DimI3 As Integer
DimI4 As Integer
DimI5 As Integer

IfN
BigAdd= 1
Else
BigAdd= N + BigAdd (N — 1)
EndIf
EndFunction

Есливы не уверены, нужна ли переменная, используйтеоператор OptionExplicitи закомментируйтеопределениепеременной.При попыткевыполнитьпрограмму,Visual Basicсообщит обошибке, еслипеременнаяиспользуетсяв программе.
Вы такжеможете уменьшитьиспользованиестека за счетпримененияглобальныхпеременных.Если вы определитепеременныев секции Declarationsмодуля вместотого, чтобыопределятьих в подпрограмме, то системе непонадобитсяотводить памятьпри каждомвызове подпрограммы.
Лучшимрешением будетопределениепеременныхв процедурепри помощизарезервированногослова Static.Статическиепеременныеиспользуютсясовместно всемиэкземплярамипроцедуры, исистеме ненужно отводитьпамять подновые копиипеременныхпри каждомвызове подпрограммы.Необоснованноеприменениерекурсии
Менееочевиднойопасностьюявляетсянеобоснованноеприменениерекурсии. Приэтом использованиерекурсии неявляется наилучшимспособом решениязадачи. Приведенныевыше функциифакториала, наибольшегообщего делителя, чисел Фибоначчии функции BigAddне обязательнодолжны бытьрекурсивными.Лучшие, нерекурсивныеверсии этихфункций описываютсяпозже в этойглаве.

=====98

В случаефакториалаи наибольшегообщего делителя, ненужная рекурсияявляется побольшей частибезвредной.Обе эти функциивыполняютсядостаточнобыстро длядостаточнобольших выходныхзначений. Ихвыполнениетакже не будетограниченоразмером стека, если вы неиспользовалибольшую частьстековогопространствав других частяхпрограммы.
С другойстороны, применениерекурсии ухудшаеталгоритм вычислениячисел Фибоначчи.Для вычисленияFib(N), алгоритм вначалевычисляетFib(N - 1)и Fib(N - 2).Но для вычисленияFib(N- 1) он долженсначала вычислитьFib(N- 2) и Fib(N- 3). При этомFib(N- 2) вычисляетсядважды.
Предыдущийанализ этогоалгоритмапоказал, чтоFib(1)и Fib(0)вычисляютсяFib(N+ 1) раз вовремя вычисленияFib(N).Так как Fib(30)= 832.040 то, чтобывычислитьFib(29), приходитсявычислять однии те же значенияFib(0)и Fib(1)832.040 раз. Алгоритмвычислениячисел Фибоначчитратит огромноеколичествовремени навычислениеэтих промежуточныхзначений сноваи снова.
В функцииBigAddсуществуетдругая проблема.Хотя она выполняетсябыстро, онаприводит кбольшой глубиневложенностирекурсии, иочень быстроприводит кисчерпаниюстековогопространства.Если бы непереполнениестека, то этафункция моглабы вычислятьрезультатыдля большихвходных значений.
Похожаяпроблема существуети в функциифакториала.Для входногозначения Nглубина рекурсиидля факториалаи функции BigAddравна N.Функция факториалане может бытьвычислена длятаких большихвходных значений, которые допустимыдля функцииBigAdd.Максимальноезначение факториала, которое можетуместитьсяв переменнойтипа double, равно 170! 7,257E+306, поэтому этонаибольшеезначение, котороеможет вычислитьэта функция.Хотя эта функцияприводит кглубокой рекурсии, она вызываетпереполнениедо того, какнаступит переполнениестека.Когданужно использоватьрекурсию
Этирассуждениямогут заставитьвас думать, чторекурсия всегданежелательна.Но это определенноне так. Многиеалгоритмыявляются рекурсивнымипо своей природе.И хотя любойалгоритм можнопереписатьтак, чтобы онне содержалрекурсии, многиеалгоритмысложнее понимать, анализировать, отлаживатьи поддерживать, если они написанынерекурсивно.
В следующихразделах приведеныметоды устранениярекурсии излюбого алгоритма.Некоторые изполученныхнерекурсивныхалгоритмовтакже простыв понимании.Функции, вычисляющиебез применениярекурсии факториал, наибольшийобщий делитель, числа Фибоначчи, и функцию BigAdd, относительнопросты.
С другойстороны, нерекурсивныеверсии алгоритмовпостроенийкривых Гильбертаи Серпинскогонамного сложнее.Их труднеепонять, поддерживать, и они дажевыполняютсянемного медленнее, чем рекурсивныеверсии. Ониприведены лишьдля того, чтобыпродемонстрироватьметоды, которыевы можетеиспользоватьдля устранениярекурсии изсложных алгоритмов, а не потому, что они лучше, чем рекурсивныеверсии соответствующихалгоритмов.
Еслиалгоритм рекурсивенпо природе, записывайтеего с использованиемрекурсии. Влучшем случае, вы не встретитесьни одной изописанныхпроблем. Еслиже вы столкнетесьс некоторымииз них, вы сможетепереписатьалгоритм безиспользованиярекурсии припомощи методов, представленныхв следующихразделах. Переписатьалгоритм частогораздо проще, чем с самогоначала написатьего без применениярекурсии.
    продолжение
--PAGE_BREAK--
======99
Хвостоваярекурсия
Вспомнимпредставленныеранее функциидля вычисленияфакториалови наибольшегообщего делителя, а также функциюBigAdd, которая приводитк переполнениюстека даже дляотносительнонебольшихвходных значений.

PrivateFunction Factorial(num As Integer) As Integer
Ifnum
Factorial= 1
Else
Factorial= num * Factorial(num — 1)
EndIf
EndFunction

PrivateFunction GCD(A As Integer, B As Integer) As Integer
IfB Mod A = 0 Then
GCD= A
Else
GCD= GCD(B Mod A, A)
EndIf
EndFunction

PrivateFunction BigAdd(N As Double) As Double
IfN
BigAdd= 1
Else
BigAdd= N + BigAdd(N — 1)
EndIf
EndFunction

Во всехэтих функциях, последнеедействие передзавершениемфункции — эторекурсивныйшаг. Этот типрекурсии вконце процедурыназываетсяхвостовойрекурсией(tail recursionили end recursion).
Таккак после рекурсиив процедуреничего не происходит, существуетпростой способее устранения.Вместо рекурсивноговызова функции, процедурасбрасываетсвои параметры, устанавливаяте, которые быона получилапри рекурсивномвызове, и затемвыполняетсяснова.
Рассмотримобщий случайрекурсивнойпроцедуры:

PrivateSub Recurse(A As Integer)
'Выполняютсякакие либодействия, вычисляетсяB, и т.д.
RecurseB
EndSub

======100

Этупроцедуру можнопереписатьбез рекурсиикак:

PrivateSub NoRecurse(A As Integer)
DoWhile (not done)
'Выполняютсякакие либодействия, вычисляетсяB, и т.д.
A= B
Loop
EndSub

Этапроцедураназываетсяустранениемхвостовойрекурсии (tailrecursion removalили end recursionremoval). Этотприем не изменяетвремя выполненияпрограммы.Рекурсивныешаги простозаменяютсяпроходами вцикле While.
Устранениехвостовойрекурсии, темне менее, устраняетвызовы подпрограмм, и поэтому можетувеличитьскорость работыалгоритма. Чтоболее важно, этот методтакже уменьшаетиспользованиестека. Алгоритмытипа функцииBigAdd, которые ограниченыглубиной рекурсии, могут от этогозначительновыиграть.
Некоторыекомпиляторыавтоматическиустраняютхвостовуюрекурсию, нокомпиляторVisual Basicэтого не делает.В противномслучае, функцияBigAdd, приведеннаяв предыдущемразделе, неприводила бык переполнениюстека.
Используяустранениехвостовойрекурсии, легкопереписатьфункции факториала, наибольшегообщего делителя, и BigAddбез рекурсии.Эти версиииспользуютзарезервированноеслово ByValдля сохранениязначений своихпараметровдля вызывающейпроцедуры.

PrivateFunction Factorial(ByVal N As Integer) As Double
Dimvalue As Double

value= 1# ' Это будетзначениемфункции.
DoWhile N > 1
value= value * N
N= N — 1 ' Подготовитьаргументы для«рекурсии».
Loop
Factorial= value
EndFunction

PrivateFunction GCD(ByVal A As Double, ByVal B As Double) As Double
DimB_Mod_A As Double

B_Mod_A= B Mod A
DoWhile B_Mod_A 0
'Подготовитьаргументы для«рекурсии».
B= A
A= B_Mod_A
B_Mod_A= B Mod A
Loop
GCD= A
EndFunction

PrivateFunction BigAdd(ByVal N As Double) As Double
Dimvalue As Double

value= 1# ' ' Это будетзначениемфункции.
DoWhile N > 1
value= value + N
N= N — 1 ' подготовитьпараметры для«рекурсии».
Loop
BigAdd= value
EndFunction

=====101

Дляалгоритмоввычисленияфакториалаи наибольшегообщего делителяпрактическине существуетразницы междурекурсивнойи нерекурсивнойверсиями. Обеверсии выполняютсядостаточнобыстро, и обеони могут оперироватьзадачами большойразмерности.
Дляфункции BigAdd, тем не менее, разница огромна.Рекурсивнаяверсия приводитк переполнениюстека даже длядовольно небольшихвходных значений.Посколькунерекурсивнаяверсия не используетстек, она можетвычислятьрезультат длязначений Nвплоть до 10154.После этогонаступит переполнениедля данных типаdouble.Конечно, выполнение10154 шагов алгоритмазаймет оченьмного времени, поэтому возможновы не станетепроверять этотфакт сами. Заметимтакже, что значениеэтой функциисовпадает созначением болеепросто вычисляемойфункции N * N(N + 1) / 2.
ПрограммыFacto2,GCD2и BigAdd2демонстрируютэти нерекурсивныеалгоритмы.Нерекурсивноевычислениечисел Фибоначчи
К сожалению, нерекурсивныйалгоритм вычислениячисел Фибоначчине содержиттолько хвостовуюрекурсию. Этоталгоритм используетдва рекурсивныхвызова длявычислениязначения, ивторой вызовследует послезавершенияпервого. Посколькупервый вызовне находитсяв самом концефункции, то этоне хвостоваярекурсия, и отее нельзя избавиться, используя приемустраненияхвостовойрекурсии.
Этоможет бытьсвязано и стем, что ограничениерекурсивногоалгоритмавычислениячисел Фибоначчисвязано с тем, что он вычисляетслишком многопромежуточныхзначений, а неглубиной вложенностирекурсии. Устранениехвостовойрекурсии уменьшаетглубину рекурсии, но оно не изменяетвремя выполненияалгоритма. Дажеесли бы устранениехвостовойрекурсии былобы применимок алгоритмувычислениячисел Фибоначчи, этот алгоритмвсе равно осталсябы чрезвычайномедленным.
Проблемаэтого алгоритмав том, что онмногократновычисляет однии те же значения.Значения Fib(1)и Fib(0)вычисляютсяFib(N+ 1) раз, когдаалгоритм вычисляетFib(N).Для вычисленияFib(29), алгоритм вычисляетодни и те жезначения Fib(0)и Fib(1)832.040 раз.
Посколькуалгоритм многократновычисляет однии те же значения, следует найтиспособ избежатьповторениявычислений.Простой иконструктивныйспособ сделатьэто — построитьтаблицу вычисленныхзначений. Когдапонадобитсяпромежуточноезначение, можнобудет взятьего из таблицы, вместо того, чтобы вычислятьего заново.

=====102

В этомпримере можносоздать таблицудля хранениязначений функцииФибоначчиFib(N)для N, не превосходящих1477. Для N>= 1477 происходитпереполнениепеременныхтипа double, используемыхв функции. Следующийкод содержитизмененнуютаким образомфункцию, вычисляющуючисла Фибоначчи.

ConstMAX_FIB = 1476 ' Максимальноезначение.

DimFibValues(0 To MAX_FIB) As Double

PrivateFunction Fib(N As Integer) As Double
'Вычислитьзначение, еслионо не находитсяв таблице.
IfFibValues(N)
FibValues(M)= Fib(N — 1) + Fib(N — 2)

Fib= FibValues(N)
EndFunction

Призапуске программы, она присваиваеткаждому элементув массиве FibValuesзначение -1. Затемона присваиваетFibValues(0)значение 0, иFibValues(1) —значение 1. Этоусловия остановкирекурсии.
Привыполнениифункции, онапроверяет, находится лиуже в массивезначение, котороеей требуется.Если его тамнет, она, как ираньше, рекурсивновычисляет этозначение исохраняет егов массиве длядальнейшегоиспользования.
ПрограммаFibo2используетэтот метод длявычислениячисел Фибоначчи.Программа можетбыстро вычислитьFib(N)для Nдо 100 или 200. Но есливы попытаетесьвычислитьFib(1476), то программавыполнитпоследовательностьрекурсивныхвызовов глубиной1476 уровней, котораявероятно переполнитстек вашейсистемы.
Тем неменее, по меретого, как программавычисляет новыезначения, оназаполняетмассив FibValues.Значения измассива позволяютфункции вычислятьвсе большиеи большие значениябез глубокойрекурсии. Например, если вычислитьпоследовательноFib(100),Fib(200),Fib(300), и т.д. то, в концеконцов, можнобудет заполнитьмассив значенийFibValuesи вычислитьмаксимальноевозможно значениеFib(1476).
Процессмедленногозаполнениямассива FibValuesприводит кновому методувычислениячисел Фибоначчи.Когда программаинициализируетмассив FibValues, она может заранеевычислить всечисла Фибоначчи.

PrivateSub InitializeFibValues()
Dimi As Integer

FibValues(0)= 0 ' Инициализацияусловий остановки.
FibValues(1)= 1
Fori = 2 To MAX_FIB
FibValues(i)= FibValues(i — 1) + FibValues(i — 2)
Nexti
EndSub

PrivateFunction Fib(N As Integer) As Duble
Fib- FibValues(N)
EndFunction

=====104

Определенноевремя в этомалгоритмезанимает составлениемассива с табличнымизначениями.Но после тогокак массивсоздан, дляполученияэлемента измассива требуетсявсего один шаг.Ни процедураинициализации, ни функция Fibне используютрекурсию, поэтомуни одна из нихне приведетк исчерпаниюстековогопространства.Программа Fibo3демонстрируетэтот подход.
Стоитупомянуть ещеодин методвычислениячисел Фибоначчи.Первое рекурсивноеопределениефункции Фибоначчииспользуетподход сверхувниз. Для получениязначения Fib(N), алгоритм рекурсивновычисляет Fib(N- 1) и Fib(N- 2) и затемскладываетих.
ПодпрограммаInitializeFibValues, с другой стороны, работает снизувверх. Она начинаетсо значенийFib(0)и Fib(1).Она затем используетменьшие значениядля вычислениябольших, до техпор, пока таблицане заполнится.
Вы можетеиспользоватьтот же подходснизу вверхдля прямоговычислениязначений функцииФибоначчикаждый раз, когда вам потребуетсязначение. Этотметод требуетбольше времени, чем выборказначений измассива, но нетребует дополнительнойпамяти длятаблицы значений.Это примерпространственно временногокомпромисса.Использованиебольшего объемапамяти дляхранения таблицызначений делаетвыполнениеалгоритма болеебыстрым.

PrivateFunction Fib(N As Integer) As Double
DimFib_i_minus_1 As Double
DimFib_i_minus_2 As Double
Dimfib_i As Double
Dimi As Integer

IfN
Fib= N
Else
Fib_i_minus_2= 0 ' ВначалеFib(0)
Fib_i_minus_1= 1 ' ВначалеFib(1)
Fori = 2 To N
fib_i= Fib_i_minus_1 + Fib_i_minus_2
Fib_i_minus_2= Fib_i_minus_1
Fib_i_minus_1= fib_i
Nexti
Fib= fib_i
EndIf
EndFunction

Этойверсии требуетсяпорядка O(N) шаговдля вычисленияFib(N).Это больше, чемодин шаг, которыйтребовалсяв предыдущейверсии, но намногобыстрее, чемO(Fib(N)) шаговв исходнойверсии алгоритма.На компьютерес процессоромPentium с тактовойчастотой 90 МГц, исходномурекурсивномуалгоритмупотребовалосьпочти 52 секундыдля вычисленияFib(32)= 2.178.309. ВремявычисленияFib(1476)1,31E+308при помощинового алгоритмапренебрежимомало. ПрограммаFibo4используетэтот метод длявычислениячисел Фибоначчи.

=====105
Устранениерекурсии вобщем случае
Функциифакториала, наибольшегообщего делителя, и BigAddможно упроститьустранениемхвостовойрекурсии. Функцию, вычисляющуючисла Фибоначчи, можно упростить, используятаблицу значенийили переформулировавзадачу с использованиемподхода снизувверх.
Некоторыерекурсивныеалгоритмынастолькосложны, то применениеэтих методовзатрудненоили невозможно.Достаточносложно былобы написатьнерекурсивныйалгоритм дляпостроениякривых Гильбертаили Серпинскогос нуля. Другиерекурсивныеалгоритмы болеепросты.
Ранеебыло показано, что алгоритм, который рисуеткривые Гильбертаили Серпинского, должен включатьпорядка O(N4)шагов, так чтоисходные рекурсивныеверсии достаточнохороши. Онидостигают почтимаксимальнойвозможнойпроизводительностипри приемлемойглубине рекурсии.
Тем неменее, встречаютсядругие сложныеалгоритмы, которые имеютвысокую глубинувложенностирекурсии, нок которым неприменимоустранениехвостовойрекурсии. Вэтом случае, все еще возможнопреобразованиерекурсивногоалгоритма внерекурсивный.
Основнойподход при этомзаключаетсяв том, чтобырассмотретьпорядок выполнениярекурсии накомпьютереи затем попытатьсясымитироватьшаги, выполняемыекомпьютером.Затем новыйалгоритм будетсам осуществлять«рекурсию»вместо того, чтобы всю работувыполнял компьютер.
Посколькуновый алгоритмвыполняетпрактическите же шаги, чтои компьютер, можно поинтересоваться, возрастет лискорость вычислений.В Visual Basicэто обычно невыполняется.Компьютер можетвыполнятьзадачи, которыетребуются прирекурсии, быстрее, чем вы можетеих имитировать.Тем не менее, оперированиеэтими деталямисамостоятельнообеспечиваетлучший контрольнад выделениемпамяти подлокальныепеременные, и позволяетизбежать глубокогоуровня вложенностирекурсии.
Обычно, при вызовеподпрограммы, система выполняеттри вещи. Во первых, сохраняетданные, которыенужны ей дляпродолжениявыполненияпосле завершенияподпрограммы.Во вторых, онапроводит подготовкук вызову подпрограммыи передает ейуправление.В третьих, когдавызываемаяпроцедуразавершается, система восстанавливаетданные, сохраненныена первом шаге, и передаетуправлениеназад в соответствующуюточку программы.Если вы преобразуетерекурсивнуюпроцедуру внерекурсивную, вам приходитсявыполнять этитри шага самостоятельно.
Рассмотримследующуюобобщеннуюрекурсивнуюпроцедуру:

SubSubr(num)
Subr()
EndSub

Посколькупосле рекурсивногошага есть ещеоператоры, выне можетеиспользоватьустранениехвостовойрекурсии дляэтого алгоритма.

=====105

Вначалепометим первыестроки в 1 и 2 блокахкода. Затем этиметки будутиспользоватьсядля определенияместа, с котороготребуетсяпродолжитьвыполнениепри возвратеиз «рекурсии».Эти меткииспользуютсятолько длятого, чтобыпомочь вампонять, чтоделает алгоритм —они не являютсячастью кодаVisual Basic. Вэтом примереметки будутвыглядеть так:

SubSubr(num)
1
Subr()
2
EndSub

Используемспециальнуюметку «0» дляобозначенияконца «рекурсии».Теперь можнопереписатьпроцедуру безиспользованиярекурсии, например, так:

SubSubr(num)
Dimpc As Integer ' Определяет, где нужно продолжитьрекурсию.

pc= 1 ' Начать сначала.
Do
SelectCase pc
Case1
If(достигнутоусловие остановки)Then
'Пропуститьрекурсию иперейти к блоку2.
pc= 2
Else
'Сохранитьпеременные, нужные послерекурсии.
'Сохранить pc =2. Точка, с которойпродолжится
'выполнениепосле возвратаиз «рекурсии».
'Установитьпеременные, нужные длярекурсии.
'Например, num = num — 1.
:
'Перейти к блоку1 для началарекурсии.
pc= 1
EndIf
Case2 ' Выполнить2 блок кода
pc= 0
Case0
If(это последняярекурсия)Then Exit Do
'Иначе восстановитьpc и другие переменные,
'сохраненныеперед рекурсией.
EndSelect
Loop
EndSub

======106

Переменнаяpc, которая соответствуетсчетчику программы, сообщает процедуре, какой шаг онадолжна выполнитьследующим.Например, приpc = 1, процедурадолжна выполнить1 блок кода.
Когдапроцедурадостигаетусловия остановки, она не выполняетрекурсию. Вместоэтого, онаприсваиваетpcзначение 2, ипродолжаетвыполнение2 блока кода.
Еслипроцедура недостигла условияостановки, онавыполняет«рекурсию».Для этого онасохраняетзначения всехлокальныхпеременных, которые ейпонадобятсяпозже послезавершения«рекурсии».Она также сохраняетзначение pcдля участкакода, которыйона будет выполнятьпосле завершения«рекурсии».В этом примереследующимвыполняется2 блок кода, поэтомуона сохраняет2 в качествеследующегозначения pc.Самый простойспособ сохранениязначений локальныхпеременныхи pcсостоит виспользованиистеков, подобныхтем, которыеописывалисьв 3 главе.
Реальныйпример поможетвам понять этусхему. Рассмотримслегка измененнуюверсию функциифакториала.В нем переписанатолько подпрограмма, которая возвращаетсвое значениепри помощипеременной, а не функции, для упрощенияработы.

PrivateSub Factorial(num As Integer, value AsInteger)
Dimpartial As Integer
1 Ifnum
value= 1
Else
Factorial(num- 1, partial)
2 value= num * partial
EndIf
EndSub

Послевозврата процедурыиз рекурсии, требуетсяузнать исходноезначение переменнойnum, чтобы выполнитьоперацию умноженияvalue = num* partial.Посколькупроцедуретребуетсядоступ к значениюnumпосле возвратаиз рекурсии, она должнасохранятьзначение переменныхpcи numдо начала рекурсии.
Следующаяпроцедурасохраняет этизначения в двухстеках на основемассивов. Приподготовкек рекурсии, онапроталкиваетзначения переменныхnumи pcв стеки. Послезавершениярекурсии, онавыталкиваетдобавленныепоследнимизначения изстеков. Следующийкод демонстрируетнерекурсивнуюверсию подпрограммывычисленияфакториала.

PrivateSub Factorial(num As Integer, value As Integer)
ReDimnum_stack(1 to 200) As Integer
ReDimpc_stack(1 to 200) As Integer
Dimstack_top As Integer ' Вершинастека.
Dimpc As Integer

pc= 1
Do
SelectCase pc
Case1
Ifnum
pc= 0 ' Конец рекурсии.
Else 'Рекурсия.
' Сохранить numи следующеезначение pc.
stack_top= stack_top + 1
num_stack(stack_top)= num
pc_stack(stack_top)= 2 ' Возобновитьс 2.
' Начать рекурсию.
num= num — 1
' Перенестиблок управленияв начало.
pc= 1
EndIf
Case2
'value содержитрезультатпоследней
'рекурсии. Умножитьего на num.
value= value * num
'«Возврат» из«рекурсии».
pc= 0
Case0
'Конец «рекурсии».
'Если стекипусты, исходныйвызов
'подпрограммызавершен.
Ifstack_top
'Иначе восстановитьлокальныепеременныеи pc.
num= num_stack(stack_top)
pc= pc_stack(stack_top)
stack_top= stacK_top — 1
EndSelect
Loop
EndSub
2блок>1блок>2блок>параметры>1блок>2блок>параметры>1блок>    продолжение
--PAGE_BREAK--
Также, как и устранениехвостовойрекурсии, этотметод имитируетповедениерекурсивногоалгоритма.Процедуразаменяет каждыйрекурсивныйвызов итерациейцикла While.Поскольку числошагов остаетсятем же самым, полное времявыполненияалгоритма неизменяется.
Также, как и в случаес устранениемхвостовойрекурсии, этотметод устраняетглубокую рекурсию, которая можетпереполнитьстек.Нерекурсивноепостроениекривых Гильберта
Примервычисленияфакториалаиз предыдущегораздела превратилпростую, нонеэффективнуюрекурсивнуюфункцию вычисленияфакториалав сложную инеэффективнуюнерекурсивнуюпроцедуру.Намного лучшийнерекурсивныйалгоритм вычисленияфакториала, был представленранее в этойглаве.

=======107-108

Можетоказатьсядостаточнотрудно найтипростую нерекурсивнуюверсию дляболее сложныхалгоритмов.Методы из предыдущегораздела могутбыть полезны, если алгоритмсодержит многократнуюили косвеннуюрекурсию.
В качествеболее интересногопримера, рассмотримнерекурсивныйалгоритм построениякривых Гильберта.

PrivateSub Hilbert(depth As Integer, Dx As Single, Dy As Single)
Ifdepth > 1 Then Hilbert depth — 1, Dy, Dx
HilbertPicture.Line-Step(Dx, Dy)
Ifdepth > 1 Then Hilbert depth — 1, Dx, Dy
HilbertPicture.Line-Step(Dy, Dx)
Ifdepth > 1 Then Hilbert depth — 1, Dx, Dy
HilbertPicture.Line-Step(-Dx, -Dy)
Ifdepth > 1 Then Hilbert depth — 1, -Dy, -Dx
EndSub

В следующемфрагменте кодапервые строкикаждого блокакода междурекурсивнымишагами пронумерованы.Эти блоки включаютпервую строкупроцедуры илюбые другиеточки, в которыхможет понадобитьсяпродолжитьвыполнениепосле возвратапосле «рекурсии».

PrivateSub Hilbert(depth As Integer, Dx As Single, Dy As Single)
1 Ifdepth > 1 Then Hilbert depth — 1, Dy, Dx
2 HilbertPicture.Line-Step(Dx, Dy)
Ifdepth > 1 Then Hilbert depth — 1, Dx, Dy
3 HilbertPicture.Line-Step(Dy, Dx)
Ifdepth > 1 Then Hilbert depth — 1, Dx, Dy
4 HilbertPicture.Line-Step(-Dx, -Dy)
Ifdepth > 1 Then Hilbert depth — 1, -Dy, -Dx
EndSub

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

====109

ConstSTACK_SIZE =20
DimDepthStack(0 To STACK_SIZE)
DimDxStack(0 To STACK_SIZE)
DimDyStack(0 To STACK_SIZE)
DimPCStack(0 To STACK_SIZE)
DimTopOfStack As Integer

PrivateSub SaveValues (Depth As Integer, Dx As Single, _
DyAs Single, pc As Integer)
TopOfStack= TopOfStack + 1
DepthStack(TopOfStack)= Depth
DxStack(TopOfStack)= Dx
DyStack(TopOfStack)= Dy
PCStack(TopOfStack)= pc
EndSub

PrivateSub RestoreValues (Depth As Integer, Dx As Single, _
DyAs Single, pc As Integer)
Depth= DepthStack(TopOfStack)
Dx= DxStack(TopOfStack)
Dy= DyStack(TopOfStack)
pc= PCStack(TopOfStack)
TopOfStack= TopOfStack — 1

EndSub

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

PrivateSub Hilbert(Depth As Integer, Dx AsSingle, Dy AsSingle)
Dimpc As Integer
Dimtmp As Single

pc= 1
Do
SelectCase pc
Case1
IfDepth > 1 Then ' Рекурсия.
'Сохранитьтекущие значения.
SaveValuesDepth, Dx, Dy, 2
'Подготовитьсяк рекурсии.
Depth= Depth — 1
tmp= Dx
Dx= Dy
Dy= tmp
pc= 1 ' Перейти вначало рекурсивноговызова.
Else 'Условие остановки.
'Достаточноглубокий уровеньрекурсии.
'Продолжитьсо 2 блоком кода.
pc= 2
EndIf
Case2
HilbertPicture.Line-Step(Dx, Dy)
IfDepth > 1 Then ' Рекурсия.
'Сохранитьтекущие значения.
SaveValuesDepth, Dx, Dy, 3
'Подготовитьсяк рекурсии.
Depth= Depth — 1
'Dx и Dy остаютсябез изменений.
pc= 1 Перейти в началорекурсивноговызова.
Else 'Условие остановки.
'Достаточноглубокий уровеньрекурсии.
'Продолжитьс 3 блоком кода.
pc= 3
EndIf
Case3
HilbertPicture.Line-Step(Dy, Dx)
IfDepth > 1 Then ' Рекурсия.
'Сохранитьтекущие значения.
SaveValuesDepth, Dx, Dy, 4
'Подготовитьсяк рекурсии.
Depth= Depth — 1
'Dx и Dy остаютсябез изменений.
pc= 1 Перейти в началорекурсивноговызова.
Else 'Условие остановки.
'Достаточноглубокий уровеньрекурсии.
'Продолжитьс 4 блоком кода.
pc= 4
EndIf
Case4
HilbertPicture.Line-Step(-Dx, -Dy)
IfDepth > 1 Then ' Рекурсия.
'Сохранитьтекущие значения.
SaveValuesDepth, Dx, Dy, 0
'Подготовитьсяк рекурсии.
Depth= Depth — 1
tmp= Dx
Dx= -Dy
Dy= -tmp
pc= 1 Перейти в началорекурсивноговызова.
Else 'Условие остановки.
'Достаточноглубокий уровеньрекурсии.
'Конец этогорекурсивноговызова.
pc= 0
EndIf
Case0 ' Возврат изрекурсии.
IfTopOfStack > 0 Then
RestoreValuesDepth, Dx, Dy, pc
Else
'Стек пуст. Выход.
ExitDo
EndIf
EndSelect
Loop
EndSub

======111

Времявыполненияэтого алгоритмаможет бытьнелегко оценитьнепосредственно.Посколькуметоды преобразованиярекурсивныхпроцедур внерекурсивныене изменяютвремя выполненияалгоритма, этапроцедура также, как и предыдущаяверсия, имеетвремя выполненияпорядка O(N4).
ПрограммаHilbert2демонстрируетнерекурсивныйалгоритм построениякривых Гильберта.Задавайтевначале построениенесложныхкривых (меньше6 порядка), покане узнаете, насколькобыстро будетвыполнятьсяэта программана вашем компьютере.Нерекурсивноепостроениекривых Серпинского
Приведенныйранее алгоритмпостроениякривых Серпинскоговключает в себякосвенную имножественнуюрекурсию. Таккак алгоритмсостоит изчетырех подпрограмм, которые вызываютдруг друга, тонельзя простопронумероватьважные строки, как это можнобыло сделатьв случае алгоритмапостроениякривых Гильберта.С этой проблемойможно справиться, слегка изменивалгоритм.
Рекурсивнаяверсия этогоалгоритмасостоит изчетырех подпрограммSierpA,SierpB,SierpCи SierpD.ПодпрограммаSierpAвыглядит так:

PrivateSub SierpA(Depth As Integer, Dist AsSingle)
IfDepth = 1 Then
Line-Step(-Dist, Dist)
Line-Step(-Dist, 0)
Line-Step(-Dist, -Dist)
Else
SierpADepth — 1, Dist
Line-Step(-Dist, Dist)
SierpBDepth — 1, Dist
Line-Step(-Dist, 0)
SierpDDepth — 1, Dist
Line-Step(-Dist, -Dist)
SierpADepth — 1, Dist
EndIf
EndSub

Тридругие процедурыаналогичны.Несложно объединитьэти четырепроцедуры водну подпрограмму.

PrivateSub SierpAll(Depth As Integer, Dist As Single, Func As Integer)
SelectCase Punc
Case1 ' SierpA
Case2 ' SierpB
Case3 ' SierpC
Case4 ' SierpD
EndSelect
EndSub

======112

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

'Код SierpA.
11 IfDepth = 1 Then
Line-Step(-Dist, Dist)
Line-Step(-Dist, 0)
Line-Step(-Dist, -Dist)
Else
SierpADepth — 1, Dist
12 Line-Step(-Dist, Dist)
SierpBDepth — 1, Dist
13 Line-Step(-Dist, 0)
SierpDDepth — 1, Dist
14 Line-Step(-Dist, -Dist)
SierpADepth — 1, Dist
EndIf

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

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

======113

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

PrivateSub SierpAll(Depth As Integer, pc As Integer)
Do
SelectCase pc
' **********
' * SierpA *
' **********
Case11
IfDepth
SierpPicture.Line-Step(-Dist, Dist)
SierpPicture.Line-Step(-Dist, 0)
SierpPicture.Line-Step(-Dist, -Dist)
pc= 0
Else
SaveValuesDepth, 12 ' ВыполнитьSierpA
Depth= Depth — 1
pc= 11
EndIf
Case12
SierpPicture.Line-Step(-Dist, Dist)
SaveValuesDepth, 13 ' ВыполнитьSierpB
Depth= Depth — 1
pc= 21
Case13
SierpPicture.Line-Step(-Dist, 0)
SaveValuesDepth, 14 ' ВыполнитьSierpD
Depth= Depth — 1
pc= 41
Case14
SierpPicture.Line-Step(-Dist, -Dist)
SaveValuesDepth, 0 ' ВыполнитьSierpA
Depth= Depth — 1
pc= 11

' Код дляSierpB, SierpC и SierpD опущен.
:

' *******************
' * Конец рекурсии.*
' *******************
Case0
IfTopOfStack
RestoreValuesDepth, pc
EndSelect
Loop
EndSub

=====114
Также, как и в случаес алгоритмомпостроениякривых Гильберта, преобразованиеалгоритмапостроениякривых Серпинскогов нерекурсивнуюформу не изменяетвремя выполненияалгоритма.Новая версияалгоритмаимитируетрекурсивныйалгоритм, которыйвыполняетсяза время порядкаO(N4), поэтомупорядок временивыполненияновой версиитакже составляетO(N4). Она выполняетсянемного медленнее, чем рекурсивнаяверсия, и являетсянамного болеесложной.
Нерекурсивнаяверсия такжемогла бы рисоватькривые болеевысоких порядков, но построениекривых Серпинскогос порядком выше8 или 9 непрактично.Все эти фактыопределяютпреимуществорекурсивногоалгоритма.
ПрограммаSierp2используетэтот нерекурсивныйалгоритм дляпостроениякривых Серпинского.Задавайтевначале построениенесложныхкривых (меньше6 порядка), покане определите, насколькобыстро будетвыполнятьсяэта программана вашем компьютере.Резюме
Приприменениирекурсивныхалгоритмовследует избегатьтрех основныхопасностей:
Бесконечной рекурсии. Убедитесь, что условия остановки вашего алгоритма прекращают все рекурсивные пути.
Глубокой рекурсии. Если алгоритм достигает слишком большой глубины рекурсии, он может привести к переполнению стека. Минимизируйте использование стека за счет уменьшения числа определяемых в процедуре переменных, использования глобальных переменных, или определения переменных как статических. Если процедура все равно приводит к переполнению стека, перепишите алгоритм в нерекурсивном виде, используя устранение хвостовой рекурсии.
Ненужной рекурсии. Обычно это происходит, если алгоритм типа рекурсивного вычисления чисел Фибоначчи, многократно вычисляет одни и те же промежуточные значения. Если вы столкнетесь с этой проблемой в своей программе, попробуйте переписать алгоритм, используя подход снизу вверх. Если алгоритм не позволяет прибегнуть к подходу снизу вверх, создайте таблицу промежуточных значений.
Применениерекурсии невсегда неправильно.Многие задачиявляются рекурсивнымипо своей природе.В этих случаяхрекурсивныйалгоритм будетпроще понять, отлаживатьи поддерживать, чем его нерекурсивнуюверсию. В качествепримера можнопривести алгоритмыпостроениякривых Гильбертаи Серпинского.Оба по своейприроде рекурсивныи намного понятнее, чем их нерекурсивныемодификации.При этом рекурсивныеверсии дажевыполняютсянемного быстрее.
Еслиу вас есть алгоритм, который рекурсивенпо своей природе, но вы не уверены, будет ли рекурсивнаяверсия лишенапроблем, запишитеалгоритм врекурсивномвиде и выяснитеэто. Может быть, проблемы невозникнут. Еслиже они возникнут, то, возможно, окажется прощепреобразоватьэту рекурсивнуюверсию в нерекурсивную, чем написатьнерекурсивнуюверсию с нуля.

======115
Глава6. Деревья
Во 2 главеприводилисьспособы созданиядинамическихсвязных структур, таких, какизображенныена рис 6.1. Такиеструктурыданных называютсяграфами (graphs).В 12 главе алгоритмыработы с графамии сетями обсуждаютсяболее подробно.В этой главерассматриваютсяграфы особоготипа, которыеназываютсядеревьями(trees).
В началеэтой главыприводитсяопределениедерева и разъясняютсянекоторыетермины. Затемв ней описываютсянекоторыеметоды реализациидеревьев различныхтипов на языкеVisual Basic. Впоследующихразделахрассматриваетсянесколькоалгоритмовобхода длядеревьев, записанныхв этих разныхформатах. Главазаканчиваетсяобсуждениемнекоторыхспециальныхтипов деревьев, включая упорядоченныедеревья (sortedtrees), деревьясо ссылками(threadedtrees), боры(tries) и квадродеревья(quadtrees).
В 7 и 8главе обсуждаютсяболее сложныетемы — сбалансированныедеревья и деревьярешений.

@Рис.6.1. Графы

=====117
Определения
Можнорекурсивноопределитьдерево как:
Пустую структуру или
Узел, называемый корнем (node) дерева, связанный с нулем или более поддеревьев (subtrees).
На рис.6.2 показано дерево.Корневой узелA связан с тремяподдеревьями, начинающимисяв узлах B, C и D. Этиузлы связаныс поддеревьямис корнями E, F иG, и эти узлы, всвою очередьсвязаны споддеревьямис корнями H, I иJ.
Терминологиядеревьев представляетсобой смесьтерминов, позаимствованныхиз ботаникии генеалогии.Из ботаникипришли термины, такие как узел(node), определяемыйкак точка, вкоторой можетначинатьсяветвление,ветвь (branch), определяемаякак связь междудвумя узлами, и лист (leaf) —узел, из которогоне выходятдругие ветви.
Изгенеалогиипришли термины, которые описываютродство. Еслиодин узел находитсянепосредственнонад другим, верхний узелназываетсяродителем(parent), а нижнийдочерним узлом(child). Узлы напути вверх отузла до корняназываютсяпредками(ancestors) узла.Например, нарис. 6.2 узлы E, B иA — это все предкиузла I.
Узлы, которые находятсяниже какого либоузла дерева, называютсяпотомками(descendants) этогоузла. Узлы E, H, I иJ на рис. 6.2 — этовсе потомкиузла B.
Иногдаузлы, имеющиеодного родителя, называютсяузлами братьямиили узлами сестрами(sibling nodes).
Существуетеще несколькотерминов, которыене пришли изботаники илигенеалогии.Внутреннимузлом (internalnode) называетсяузел, которыйне являетсялистом. Порядкомузла (node degree)называетсячисло его дочернихузлов. Порядокдерева — этонаибольшийпорядок егоузлов. Деревона рис. 6.2 — третьегопорядка, потомучто узлы с наибольшимпорядком, узлыA и E, имеют по 3дочерних узла.
Глубина(depth) дереваравна числуего предковплюс 1. На рис.6.2 глубина узлаE равна 3. Глубиной(depth) или высотой(height) дереваназываетсянаибольшаяглубина егоузлов. Глубинадерева на рис.6.2 равна 4.
Дерево2 порядка называетсядвоичным деревом(binary tree).Деревья третьегопорядка иногданазываютсятроичными(ternary) деревьями.Более того, деревья порядкаN иногда называютсяN ичными (N ary)деревьями.

@Рис.6.2. Дерево

======118

Деревопорядка 12, например, называется12 ричным (12 ary)деревом, а недодекадеричным(dodecadary) деревом.Некоторыеизбегают употреблениялишних терминови просто говорят«деревья 12 порядка».
Рис.6.3 иллюстрируетнекоторые изэтих терминов.Представлениядеревьев
Теперь, когда вы познакомилисьс терминологией, вы можете представитьсебе способыреализациидеревьев наязыке VisualBasic. Один изспособов —создать отдельныйкласс для каждоготипа узловдерева. Дляпостроениядерева, показанногона рис. 6.3, вы можетеопределитьструктурыданных дляузлов, которыеимеют ноль, один, два илитри дочернихузла. Этот подходбыл бы довольнонеудобным.Кроме того, чтонужно было быуправлятьчетырьмя различнымиклассами, вклассах потребовалисьбы какие тофлаги, которыебы указывалитип дочернихузлов. Алгоритмы, которые оперировалибы этими деревьями, должны былибы уметь работатьсо всем различнымитипами деревьев.Полныеузлы
В качествепростого решенияможно определитьодин тип узлов, который содержитдостаточноечисло указателейна потомковдля представлениявсех нужныхузлов. Я называюэто методомполных узлов, так как некоторыеузлы могут бытьбольшего размера, чем необходимона самом деле.
Дерево, изображенноена рис 6.3, имеет3 порядок. Дляпостроенияэтого деревас использованиемметода полныхузлов (fatnodes), требуетсяопределитьединственныйкласс, которыйсодержит указателина три дочернихузла. Следующийкод демонстрирует, как эти указателимогут бытьопределеныв классе TernaryNode.
кодSierpD>кодSierpC>кодSierpB>кодSierpA>    продолжение
--PAGE_BREAK--
PublicLeftChild As TernaryNode
PublicMiddleChild As TernaryNode
PublicRightChild As TernaryNode

@Рис.6.3. Части троичного(3 порядка) дерева

======119

Припомощи этогокласса можнопостроитьдерево, используязаписи Childузлов, для связиих друг с другом.Следующийфрагмент кодастроит дваверхних уровнядерева, показанногона рис. 6.3.

DimA As New TernaryNode
DimB As New TernaryNode
DimC As New TernaryNode
DimD As New TernaryNode
:

SetA.LeftChild = B
SetA.MiddleChild = C
SetA.RightChild = D
:

ПрограммаBinary, показаннаяна рис. 6.4, используетметод полныхузлов для работыс двоичнымдеревом. Когдавы выбираетеузел с помощьюмыши, программаподсвечиваеткнопку AddLeft (Добавитьслева), еслиузел не имеетлевого потомкаи кнопку AddRight (Добавитьсправа), еслиузел не имеетправого потомка.Кнопка Remove(Удалить) разблокируется, если выбранныйузел не являетсякорневым. Есливы нажмете накнопку Remove, программаудалит узели всех его потомков.
Посколькупрограммапозволяетсоздать узлыс нулевым числом, одним или двумядочернимиузлами, онаиспользуетпредставлениев виде полныхузлов. Вы можетелегко распространитьэтот примерна деревьяболее высокихпорядков.Спискипотомков
Еслипорядки узловв дереве сильноразличаются, метод полныхузлов приводитк напрасномурасходованиюбольшого количествапамяти. Чтобыпостроитьдерево, показанноена рис. 6.5 с использованиемполных узлов, вам понадобитсяопределитьв каждом узлепо шесть указателей, хотя тольков одном узлевсе шесть изних используются.Это представлениедерева потребует72 указателейна дочерниеузлы, из которыхв действительностибудет использоватьсятолько 11.

@Рис.6.4. ПрограммаBinary

======120

Некоторыепрограммыдобавляют иудаляют узлы, изменяя порядокузлов в процессевыполнения.В этом случаеметод полныхузлов не будетработать. Такиединамическиизменяющиесядеревья можнопредставить, поместив дочерниеузлы в списки.Есть несколькоподходов, которыеможно использоватьдля созданиясписков дочернихузлов. Наиболееочевидныйподход заключаетсяв создании вклассе узлаоткрытого(public) массивадочерних узлов, как показанов следующемкоде. Тогда дляоперированиядочернимиузлами можноиспользоватьметоды работысо спискамина основе массивов.

PublicChildren() As TreeNode
PublicNumChildren As Integer

К сожалению,Visual Basic непозволяетопределятьоткрытые массивыв классах. Этоограничениеможно обойти, определивмассив какзакрытый (private), и оперируяэлементамимассива припомощи процедурсвойств.

Privatem_Chirdren() As TreeNode
Privatem_NumChildren As Integer

PropertyGet Children(Index As Integer) As TreeNode
SetChildren = m_Children(Index)
EndProperty

PropertyGet NumChildren() As Integer
NumChildren= m_NumChildren()
EndProperty

Второйподход состоитв том, чтобысохранятьссылки на дочерниеузлы в связныхсписках. Каждыйузел содержитссылку на первогопотомка. Онтакже содержитссылку на следующегопотомка на томже уровне дерева.Эти связи образуютсвязный списокузлов одногоуровня, поэтомуя называю этотметод представлениемв виде связногосписка узловодного уровня(linked sibling).За информациейо связных спискахвы можете обратитьсяко 2 главе.

@Рис.6.5. Дерево с узламиразличныхпорядков

======121

Третийподход заключаетсяв том, чтобыопределитьв классе узлаоткрытую коллекцию, которая будетсодержатьдочерние узлы:

PublicChildren As New Collection

Эторешение позволяетиспользоватьвсе преимуществаколлекций.Программа можетпри этом легкодобавлять иудалять элементыиз коллекции, присваиватьдочерним узламключи, и использоватьоператор ForEachдля выполненияциклов со ссылкамина дочерниеузлы.
ПрограммаNAry, показаннаяна рис. 6.6, используетколлекциюдочерних узловдля работы сдеревьямипорядка N в основномтаким же образом, как программаBinaryработает сдвоичнымидеревьями. Вэтой программе, тем не менее, можно добавлятьк каждому узлулюбое количествопотомков.
Длятого чтобыизбежать чрезмерногоусложненияпользовательскогоинтерфейса, программа NAryвсегда добавляетновые узлы вконец коллекциидочерних узловродителя. Выможете модифицироватьэту программу, реализоваввставку дочернихузлов в серединуколлекции, нопользовательскийинтерфейс приэтом усложнится.Представлениенумерациейсвязей
Представлениенумерациейсвязей (forwardstar), впервыеупомянутоев 4 главе, позволяеткомпактнопредставитьдеревья, графыи сети при помощимассива. Дляпредставлениядерева нумерациейсвязей, в массивеFirstLinkзаписываетсяиндекс дляпервых ветвей, выходящих изкаждого узла.В другой массив,ToNode, заносятся узлы, к которым ведетветвь.
Сигнальнаяметка в концемассива FirstLinkуказывает наточку сразупосле последнегоэлемента массиваToNode.Это позволяетлегко определить, какие ветвивыходят изкаждого узла.Ветви, выходящиеиз узла I, находятся подномерами отFirstLink(I)до FirstLink(I+1)-1.Для выводасвязей, выходящихиз узла I, можно использоватьследующий код:

Forlink = FirstLink(I) To FirstLink(I + 1) — 1
PrintFormat$(I) & " -> " & Format$(ToNode(link))
Nextlink

@Рис.6.6. ПрограммаNary

=======123

На рис.6.7 показано деревои его представлениенумерациейсвязей. Связи, выходящие из3 узла (обозначенногобуквой D) этосвязи от FirstLink(3)до FirstLink(4)-1.Значение FirstLink(3)равно 9, а FirstLink(4) = 11, поэтому этосвязи с номерами9 и 10. Записи ToNodeдля этих связейравны ToNode(9)= 10 и ToNode(10)= 11, поэтомуузлы 10 и 11 будутдочерними для3 узла. Это узлы, обозначенныебуквами K и L. Этоозначает, чтосвязи, покидающиеузел D, ведут кузлам K и L.
Представлениедерева нумерациейсвязей компактнои основано намассиве, поэтомудеревья, представленныетаким образом, можно легкосчитывать изфайлов и записыватьв файл. Операциидля работы смассивами, которые используютсяпри такомпредставлении, также могутбыть быстрее, чем операции, нужные дляиспользованияузлов, содержащихколлекциидочерних узлов.
По этимпричинам большаячасть литературыпо сетевымалгоритмамиспользуетпредставлениенумерациейсвязей. Например, многие статьи, касающиесявычислениякратчайшегопути, предполагают, что данныенаходятся вподобном формате.Если вам когда либопридется изучатьэти алгоритмыв журналах, таких как “ManagementScience” или“Operations Research”, вам необходиморазобратьсяв этом представлении.

@Рис.6.7. Дерево и егопредставлениенумерациейсвязей

=======123

Используяпредставлениенумерациейсвязей, можнобыстро найтисвязи, выходящиеиз определенногоузла. С другойстороны, оченьсложно изменятьструктуруданных, представленныхв таком виде.Чтобы добавитьк узлу A на рис.6.7 еще одногопотомка, придетсяизменить почтивсе элементыв обоих массивахFirstLinkи ToNode.Во первых, каждый элементв массиве ToNodeнужно сдвинутьна одну позициювправо, чтобыосвободитьместо под новыйэлемент. Затем, нужно вставитьновую записьв массив ToNode, которая указываетна новый узел.И, наконец, нужнообойти массивToNode, обновив каждыйэлемент, чтобыон указывална новое положениесоответствующейзаписи ToNode.Поскольку всезаписи в массивеToNodeсдвинулисьна одну позициювправо, чтобыосвободитьместо для новойсвязи, потребуетсядобавить единицуко всем затронутымзаписям FirstLink.
На рис.6.8 показано деревопосле добавлениянового узла.Записи, которыеизменились, закрашены серымцветом.
Удалениеузла из началапредставлениянумерациейсвязей так жесложно, как ивставка узла.Если удаляемыйузел имеетпотомков, процессзанимает ещебольше времени, посколькупридется удалятьи все дочерниеузлы.
Относительнопростой классс открытойколлекциейдочерних узловлучше подходит, если нужночасто модифицироватьдерево. Обычнопроще пониматьи отлаживатьпроцедуры, которые оперируютдеревьями вэтом представлении.С другой стороны, представлениенумерациейсвязей иногдаобеспечиваетболее высокуюпроизводительностьдля сложныхалгоритмовработы с деревьями.Оно также являютсястандартнойструктуройданных, обсуждаемойв литературе, поэтому вамследует ознакомитьсяс ним, если выхотите продолжитьизучение алгоритмовработы с сетямии деревьями.

@Рис.6.8. Вставка узлав дерево, представленноенумерациейсвязей

=======124

ПрограммаFstarиспользуетпредставлениенумерациейсвязей дляработы с деревом, имеющим узлыразного порядка.Она аналогичнапрограмме NAry, за исключениемтого, что онаиспользуетпредставлениена основе массива, а не коллекций.
Есливы посмотритена код программыFstar, вы увидите, насколькосложно в нейдобавлять иудалять узлы.Следующий коддемонстрируетудаление узлаиз дерева.

SubFreeNodeAndChildren(ByVal parent As Integer, _
ByVallink As Integer, ByVal node As Integer)

'Recursively remove the node's children.
DoWhile FirstLink(node)
FreeNodeAndChildrennode, FirstLink(node), _
ToNode(FirstLink(node))
Loop

'Удалить связь.
RemoveLinkparent, link

'Удалитьсам узел.
RemoveNodenode
EndSub

SubRemoveLink(node As Integer, link As Integer)
Dimi As Integer

'Обновить записимассива FirstLink.
Fori = node + 1 To NumNodes
FirstLink(i)= FirstLink(i) — 1
Nexti

'Сдвинуть массивToNode чтобы заполнитьпустую ячейку.
Fori = link + 1 To NumLinks — 1
ToNode(i- 1) = ToNode(i)
Nexti

'Удалить лишнийэлемент изToNode.
NumLinks= NumLinks — 1
IfNumLinks > 0 Then ReDim Preserve ToNode(0 To NumLinks — 1)
EndSub

SubRemoveNode(node As Integer)
Dimi As Integer

'Сдвинуть элементымассива FirstLink, чтобызаполнить
'пустую ячейку.
Fori = node + 1 To NumNodes
FirstLink(i- 1) = FirstLink(i)
Nexti

'Сдвинуть элементымассива NodeCaption.
Fori = node + 1 To NumNodes — 1
NodeCaption(i- 1) = NodeCaption(i)
Nexti

'Обновить записимассива ToNode.
Fori = 0 To NumLinks — 1
IfToNode(i) >= node Then ToNode(i) = ToNode(i) — 1
Nexti

'Удалить лишнююзапись массиваFirstLink.
NumNodes= NumNodes — 1
ReDimPreserve FirstLink(0 To NumNodes)

ReDimPreserve NodeCaption(0 To NumNodes — 1)
UnloadFStarForm.NodeLabel(NumNodes)
EndSub

Этонамного сложнее, чем соответствующийкод в программеNAry:

PublicFunction DeleteDescendant(target As NAryNode) As Boolean
Dimi As Integer
Dimchild As NAryNode

'Является лиузел дочернимузлом.
Fori = 1 To Children.Count
IfChildren.Item(i) Is target Then
Children.Removei
DeleteDescendant= True
ExitFunction
EndIf
Nexti

'Если это недочерний узел, рекурсивно
'проверитьостальныхпотомков.
ForEach child In Children
Ifchild.DeleteDescendant(target) Then
DeleteDescendant= True
ExitFunction
EndIf
Nextchild
EndFunction

=======125-126
Полныедеревья
Полноедерево (completetree) содержитмаксимальновозможное числоузлов на каждомуровне, кроменижнего. Всеузлы на нижнемуровне сдвигаютсявлево. Например, каждый уровеньтроичногодерева содержитв точности тридочерних узла, за исключениемлистьев, и возможно, одного узлана один уровеньвыше листьев.На рис. 6.9 показаныполные двоичноеи троичноедеревья.
Полныедеревья обладаютрядом важныхсвойств. Во первых, это кратчайшиедеревья, которыемогут содержатьзаданное числоузлов. Например, двоичное деревона рис. 6.9 — одноиз самых короткихдвоичных деревьевс шестью узлами.Существуютдругие двоичныедеревья с шестьюузлами, но ниодно из них неимеет высотуменьше 3.
Во вторых, если полноедерево порядкаD состоит из Nузлов, оно будетиметь высотупорядка O(logD(N))и O(N) листьев. Этифакты имеютбольшое значение, посколькумногие алгоритмыобходят деревьясверху внизили в противоположномнаправлении.Время выполненияалгоритма, выполняющегоодно из этихдействий, будетпорядка O(N).
Чрезвычайнополезное свойствополных деревьевзаключаетсяв том, что онимогут бытьочень компактнозаписаны вмассивах. Еслипронумероватьузлы в «естественном»порядке, сверхувниз и слеванаправо, томожно поместитьэлементы деревав массив в этомпорядке. Нарис. 6.10 показано, как можно записатьполное деревов массиве.
Кореньдерева находитсяв нулевой позиции.Дочерние узлыузла I находятсяна позициях2 * I + 1 и 2 * I + 2. Например, на рис. 6.10, потомкиузла в позиции1 (узла B), находятсяв позициях 3 и4 (узлы D и E).
Легкообобщить этопредставлениена полные деревьяболее высокогопорядка D. Кореньдерева такжебудет находитьсяв позиции 0. Потомкиузла I занимаютпозиции отD * I + 1 до D * I +(I — 1).Например, втроичном дереве, потомки узлав позиции 2, будутзанимать позиции7, 8 и 9. На рис. 6.11 показанополное троичноедерево и егопредставлениев виде массива.

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

=========127

@Рис.6.10. Запись полногодвоичногодерева в массиве

Прииспользованииэтого методазаписи деревав массиве легкои просто получитьдоступ к потомкамузла. При этомне требуетсядополнительнойпамяти дляколлекцийдочерних узловили меток вслучае представлениянумерациейсвязей. Чтениеи запись деревав файл сводитсяпросто к сохранениюили чтениюмассива. Поэтомуэто несомненнолучшее представлениедерева дляпрограмм, которыесохраняютданные в полныхдеревьях.Обходдерева
Последовательноеобращение ковсем узламназываетсяобходом(traversing) дерева.Существуетнесколькопоследовательностейобхода узловдвоичногодерева. Трипростейшихиз них — прямой(divorder), симметричный(inorder), и обратный(postorder)обход, описываютсяпростыми рекурсивнымиалгоритмами.Для каждогозаданного узлаалгоритмывыполняютследующиедействия:
Прямойобход:
Обращение к узлу.
Рекурсивный прямой обход левого поддерева.
Рекурсивный прямой обход правого поддерева.
Симметричныйобход:
Рекурсивный симметричный обход левого поддерева.
Обращение к узлу.
Рекурсивный симметричный обход левого поддерева.
Обратныйобход:
Рекурсивный обратный обход левого поддерева.
Рекурсивный обратный обход правого поддерева.
Обращение к узлу.

@Рис.6.11. Запись полноготроичногодерева в массиве

=======128

Всетри порядкаобхода являютсяпримерамиобхода в глубину(depth firsttraversal). Обходначинаетсяс прохода вглубьдерева до техпор, пока алгоритмне достигнетлистьев. Привозврате изрекурсивноговызова подпрограммы, алгоритм перемещаетсяпо дереву вобратном направлении, просматриваяпути, которыеон пропустилпри движениивниз.
Обходв глубину удобноиспользоватьв алгоритмах, которые должнывначале обойтилистья. Например, метод ветвейи границ, описанныйв 8 главе, какможно быстреепытается достичьлистьев. Ониспользуетрезультаты, полученныена уровне листьевдля уменьшениявремени поискав оставшейсячасти дерева.
Четвертыйметод перебораузлов дерева —это обход вширину (breadth firsttraversal). Этотметод обращаетсяко всем узламна заданномуровне дерева, перед тем, какперейти к болееглубоким уровням.Алгоритмы, которые проводятполный поискпо дереву, частоиспользуютобход в ширину.Алгоритм поискакратчайшегомаршрута сустановкойметок, описанныйв 12 главе, представляетсобой обходв ширину, деревакратчайшегопути в сети.
На рис.6.12 показанонебольшоедерево и порядок, в которомперебираютсяузлы во времяпрямого, симметричногои обратногообхода, а такжеобхода в ширину.

@Рис.6.12. Обходы дерева

======129

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

DimNodeLabel() As String ' Записьметок узлов.
DimNumNodes As Integer

'Инициализациядерева.
:
PrivateSub Preorder(node As Integer)
PrintNodeLabel (node) ' Узел.
'Первый потомок.
Ifnode * 2 + 1
'Второй потомок.
Ifnode * 2 + 2
EndSub

PrivateSub Inorder(node As Integer)
'Первый потомок.
Ifnode * 2 + 1
PrintNodeLabel (node) ' Узел.
'Второй потомок.
Ifnode * 2 + 2
EndSub
    продолжение
--PAGE_BREAK--
PrivateSub Postorder(node As Integer)
'Первый потомок.
Ifnode * 2 + 1
'Второй потомок.
Ifnode * 2 + 2
PrintNodeLabel (node) ' Узел.
EndSub

PrivateSub BreadthFirstPrint()
Dimi As Integer

Fori = 0 To NumNodes
PrintNodeLabel(i)
Nexti
EndSub

======130

ПрограммаTrav1демонстрируетпрямой, симметричныйи обратныйобходы, а такжеобход в ширинудля двоичныхдеревьев наоснове массивов.Введите высотудерева, и нажмитена кнопку CreateTree (Создатьдерево) длясоздания полногодвоичногодерева. Затемнажмите накнопки Preorder(Прямой обход),Inorder (Симметричныйобход), Postorder(Обратный обход)или Breadth-First(Обход в ширину)для того, чтобыувидеть, какпроисходитобход дерева.На рис. 6.13 показаноокно программы, в которомотображаетсяпрямой обходдерева 4 порядка.
Прямойи обратныйобход для другихпредставленийдерева осуществляетсятак же просто.Следующий коддемонстрируетпроцедурупрямого обходадля дерева, записанногов формате снумерациейсвязей:

PrivateSub PreorderPrint(node As Integer)
Dimlink As Integer

PrintNodeLabel(node)
Forlink = FirstLink(node) To FirstLink(node + 1) — 1
PreorderPrintToNode (link)
Nextlink
EndSub

@Рис.6.13. Пример прямогообхода деревав программеTrav1

=======131

Какупоминалосьранее, сложнодать определениесимметричногообхода длядеревьев больше2 порядка. Темне менее, послетого, как выпоймете, чтоимеется в видупод симметричнымобходом, реализоватьего достаточнопросто. Следующийкод демонстрируетпроцедурусимметричногообхода, котораяобращаетсяк половинепотомков узла(с округлениемв большую сторону), затем к самомуузлу, а потом —к остальнымпотомкам.

PrivateSub InorderPrint(node As Integer)
Dimmid_link As Integer
Dimlink As Integer

'Найти среднийдочерний узел.
mid_link- (FirstLink(node + 1) — 1 + FirstLink(node)) \ 2

'Обход первойгруппы потомков.
Forlink = FirstLink(node) To mid_link
InorderPrintToNode(link)
Nextlink

'Обращение кузлу.
PrintNodeLabel(node)

'Обход второйгруппы потомков.
Forlink = mid_link + 1 To FirstLink(node + 1) — 1
InorderPrintToNode(link)
Nextlink
EndSub

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

DimRoot As TreeNode
'Инициализациядерева.
:

PrivateSub BreadthFirstPrint(}
Dimqueue As New Collection ' Очередьна основе коллекций.
Dimnode As TreeNode
Dimchild As TreeNode

'Начать с корнядерева в очереди.
queue.AddRoot

'Многократнаяобработкапервого элемента
'в очереди, покаочередь неопустеет.
DoWhile queue.Count > 0
node= queue.Item(1)
queue.Remove1

'Обращение кузлу.
PrintNodeLabel(node)

'Поместить вочередь потомковузла.
ForEach child In node.Children
queue.Addchild
Nextchild
Loop
EndSub

=====132

ПрограммаTrav2демонстрируетобход деревьев, использующихколлекциидочерних узлов.Программаявляется объединениемпрограмм Nary, которая оперируетдеревьямипорядка N, ипрограммыTrav1, которая демонстрируетобходы деревьев.
Выберитеузел, и нажмитена кнопку AddChild (Добавитьдочерний узел), чтобы добавитьк узлу потомка.Нажмите накнопки Preorder,Inorder, Postorderили BreadthFirst, чтобыувидеть примерысоответствующихобходов. Нарис. 6.14 показанапрограммаTrav2, которая отображаетобратный обход.Упорядоченныедеревья
Двоичныедеревья частоявляются естественнымспособомпредставленияи обработкиданных в компьютерныхпрограммах.Посколькумногие компьютерныеоперации являютсядвоичными, ониестественнопреобразуютсяв операции сдвоичнымидеревьями.Например, можнопреобразоватьдвоичное отношение«меньше» вдвоичное дерево.Если использоватьвнутренниеузлы деревадля обозначениятого, что «левыйпотомок меньшеправого» выможете использоватьдвоичное дереводля записиупорядоченногосписка. На рис.6.15 показанодвоичное дерево, содержащееупорядоченныйсписок с числами1, 2, 4, 6, 7, 9.

@Рис.6.14. Пример обратногообхода деревав программеTrav2

======133

@Рис.6.15. Упорядоченныйсписок: 1, 2, 4, 6, 7, 9.
Добавлениеэлементов
Алгоритмвставки новогоэлемента вдерево такоготипа достаточнопрост. Начнемс корневогоузла. По очередисравним значениявсех узлов созначениемнового элемента.Если значениенового элементаменьше илиравно значениюузла, перейдемвниз по левойветви дерева.Если новоезначение больше, чем значениеузла, перейдемвниз по правойветви. Когдаэтот процессдойдет до листа, элемент помещаетсяв эту точку.
Чтобыпоместитьзначение 8 вдерево, показанноена рис. 6.15, мы начинаемс корня, которыйимеет значение4. Поскольку 8больше, чем 4, переходим поправой ветвик узлу 9. Поскольку8 меньше 9, переходимзатем по левойветви к узлу7. Поскольку 8больше 7, сновапытаемся пойтипо правой ветви, но у этого узланет правогопотомка. Поэтомуновый элементвставляетсяв этой точке, и получаетсядерево, показанноена рис. 6.16.
Следующийкод добавляетновое значениениже узла вупорядоченномдереве. Программаначинает вставкус корня, вызываяпроцедуруInsertItemRoot,new_value.

PrivateSub InsertItem(node As SortNode, new_value As Integer)
Dimchild As SortNode

Ifnode Is Nothing Then
'Мы дошли долиста.
'Вставить элементздесь.
Setnode = New SortNode
node.Value= new_value
MaxBox= MaxBox + 1
LoadNodeLabel(MaxBox)
Setnode.Box = NodeLabel(MaxBox)
WithNodeLabel(MaxBox)
.Caption= Format$(new_value)
.Visible= True
EndWith
ElseIfnew_value
'Перейти полевой ветви.
Setchild = node.LeftChild
InsertItemchild, new_value
Setnode.LeftChild = child
Else
'Перейти поправой ветви.
Setchild = node.RightChild
InsertItemchild, new_value
Setnode.RightChild = child
EndIf
EndSub

Когдаэта процедурадостигает концадерева, происходитнечто совсемнеочевидное.В Visual Basic, когда вы передаетепараметрподпрограмме, этот параметрпередаетсяпо ссылке, есливы не используетезарезервированноеслово ByVal.Это означает, что подпрограммаработает с тойже копией параметра, которую используетвызывающаяпроцедура. Еслиподпрограммаизменяет значениепараметра, значение ввызывающейпроцедуре такжеизменяется.
КогдапроцедураInsertItemрекурсивновызывает самасебя, она передаетуказатель надочерний узелв дереве. Например, в следующихоператорахпроцедурапередает указательна правогопотомка узлав качествепараметра узлапроцедурыInsertItem.Если вызываемаяпроцедураизменяет значениепараметра узла, указатель напотомка такжеавтоматическиобновляетсяв вызывающейпроцедуре.Затем в последнейстроке кодазначение правогопотомка устанавливаетсяравным новомузначению, такчто созданныйновый узелдобавляетсяк дереву.

Setchild = node.RightChild
Insertltemchild, new_value
Setnode.RightChild = child
Удалениеэлементов
Удалениеэлемента изупорядоченногодерева немногосложнее, чемего вставка.После удаленияэлемента, программеможет понадобитьсяпереупорядочитьдругие узлы, чтобы соотношение«меньше» продолжаловыполнятьсядля всего дерева.При этом нужнорассмотретьнесколькослучаев.

=====134-135

@Рис.6.17. Удаление узлас единственнымпотомком

Во первых, если у удаляемогоузла нет потомков, вы можете простоубрать его издерева, так какпорядок оставшихсяузлов при этомне изменится.
Во вторых, если у узлавсего одиндочерний узел, вы можете поместитьего на местоудаленногоузла. Порядокостальныхпотомков удаленногоузла останетсянеизменным, поскольку ониявляются такжепотомками идочернего узла.На рис. 6.17 показанодерево, из которогоудаляется узел4, который имеетвсего одиндочерний узел.
Еслиудаляемый узелимеет два дочерних, то не обязательноодин из нихзаймет местоудаленногоузла. Если потомкиузла такжеимеют по двадочерних узла, то все потомкине смогут занятьместо удаленногоузла. Удаленныйузел имеетодного лишнегопотомка, и дочернийузел, которыйвы хотели быпоместить наего место, такжеимеет двухпотомков, такчто на узелпришлось бытри потомка.
Чтобырешить этупроблему, удаленныйузел заменяетсясамым правымузлом из левойветви. Другимисловами, нужносдвинутьсяна один шагвниз по левойветви, выходившейиз удаленногоузла. Затемнужно двигатьсяпо правым ветвямвниз до техпор, пока ненайдется узел, который неимеет правойветви. Это самыйправый узелна ветви слеваот удаляемогоузла. В дереве, показанномслева на рис.6.18, узел 3 являетсясамым правымузлом в левойот узла 4 ветви.Можно заменитьузел 4 листом3, сохранив приэтом порядокдерева.

@Рис.6.18. Удаление узла, который имеетдва дочерних

=======136

@Рис.6.19. Удаление узла, если заменяющийего узел имеетпотомка

Остаетсяпоследнийвариант — когдазаменяющийузел имеетлевого потомка.В этом случае, вы можете переместитьэтого потомкана место, освободившеесяв результатеперемещениязамещающегоузла, и деревоснова будетрасположенов нужном порядке.Уже известно, что самый правыйузел не имеетправого потомка, иначе он не былбы таковым. Этоозначает, чтоне нужно беспокоиться, не имеет лизамещающийузел двух потомков.
Этасложная ситуацияпоказана нарис. 6.19. В этомпримере удаляетсяузел 8. Самыйправый элементв его левойветви — этоузел 7, которыйимеет потомка —узел 5. Чтобысохранитьпорядок деревапосле удаленияузла 8, заменимузел 8 узлом 7, а узел 7 — узлом5. Заметьте, чтоузел 7 получаетновых потомков, а узел 5 сохраняетсвоих.
Следующийкод удаляетузел из упорядоченногодвоичногодерева:

PrivateSub DeleteItem(node As SortNode,target_value As Integer)
Dimtarget As SortNode
Dimchild As SortNode

'Если узел ненайден, вывестисообщение.
Ifnode Is Nothing Then
Beep
MsgBox«Item » & Format$(target_value) & _
"не найден вдереве."
ExitSub
EndIf

Iftarget_value
'Продолжитьдля левогоподдерева.
Setchild = node.LeftChild
DeleteItemchild, target_value
Setnode.LeftChild = child
ElseIftarget_value > node.Value Then
'Продолжитьдля правогоподдерева.
Setchild = node.RightChild
DeleteItemchild, target_value
Setnode.RightChild = child
Else
'Искомый узелнайден.
Settarget = node
Iftarget.LeftChild Is Nothing Then
'Заменить искомыйузел его правымпотомком.
Setnode = node.RightChild
ElseIftarget.RightChild Is Nothing Then
'Заменить искомыйузел его левымпотомком.
Setnode = node.LeftChild
Else
'Вызов подпрограмыReplaceRightmost для замены
'искомого узласамым правымузлом
'в его левойветви.
Setchild = node.LeftChild
ReplaceRightmostnode, child
Setnode.LeftChild = child
EndIf
EndIf
EndSub

PrivateSub ReplaceRightmost(target As SortNode, repl As SortNode)
Dimold_repl As SortNode
Dimchild As SortNode

IfNot (repl.RightChild Is Nothing) Then
'Продолжитьдвижение вправои вниз.
Setchild = repl.RightChild
ReplaceRightmosttarget, child
Setrepl.RightChild = child
Else
'Достигли дна.
'Запомнитьзаменяющийузел repl.
Setold_repl = repl

'Заменить узелrepl его левымпотомком.
Setrepl = repl.LeftChild

'Заменить искомыйузел target with repl.
Setold_repl.LeftChild = target.LeftChild
Setold_repl.RightChild = target.RightChild
Settarget = old_repl
EndIf
EndSub

======137-138

Алгоритмиспользуетв двух местахприем передачипараметровв рекурсивныеподпрограммыпо ссылке. Во первых, подпрограммаDeleteItemиспользуетэтот прием длятого, чтобыродитель искомогоузла указывална заменяющийузел. Следующиеоператорыпоказывают, как вызываетсяподпрограммаDeleteItem:

Setchild = node.LeftChild
DeleteItemchild, target_value
Setnode.LeftChild = child

Когдапроцедураобнаруживаетискомый узел(узел 8 на рис.6.19), она получаетв качествепараметра узлауказательродителя наискомый узел.Устанавливаяпараметр назамещающийузел (узел 7), подпрограммаDeleteItemзадает дочернийузел для родителятак, чтобы онуказывал нановый узел.
Следующиеоператорыпоказывают, как процедураReplaceRightMostрекурсивновызывает себя:

Setchild = repl.RightChild
ReplaceRightmosttarget, child
Setrepl.RightChild = child

Когдапроцедуранаходит самыйправый узелв левой от удаляемогоузла ветви(узел 7), в параметреreplнаходитсяуказательродителя насамый правыйузел. Когдапроцедураустанавливаетзначение replравным repl.LeftChild, она автоматическисоединяетродителя самогоправого узлас левым дочернимузлом самогоправого узла(узлом 5).
ПрограммаTreeSortиспользуетэти процедурыдля работы супорядоченнымидвоичнымидеревьями.Введите целоечисло, и нажмитена кнопку Add, чтобы добавитьэлемент к дереву.Введите целоечисло, и нажмитена кнопку Remove, чтобы удалитьэтот элементиз дерева. Послеудаления узла, дерево автоматическипереупорядочиваетсядля сохраненияпорядка «меньше».Обходупорядоченныхдеревьев
Полезноесвойствоупорядоченныхдеревьев состоитв том, что ихпорядок совпадаетс порядкомсимметричногообхода. Например, при симметричномобходе дерева, показанногона рис. 6.20, обращениек узлам происходитв порядке2-4-5-6-7-8-9.

@Рис.6.20. Симметричныйобход упорядоченногодерева: 2, 4, 5, 6, 7, 8, 9

=========139

Этосвойствосимметричногообхода упорядоченныхдеревьев приводитк простомуалгоритмусортировки:
Добавить элемент к упорядоченному дереву.
Вывести элементы, используя симметричный обход.
Этоталгоритм обычноработает достаточнохорошо. Тем неменее, еслидобавлятьэлементы кдереву в определенномпорядке, тодерево можетстать высокими тонким. Нарис. 6.21 показаноупорядоченноедерево, котороеполучаетсяпри добавлениик нему элементовв порядке 1, 6, 5, 2,3, 4. Другие последовательноститакже могутприводить кпоявлениювысоких и тонкихдеревьев.
Чемвыше становитсяупорядоченноедерево, тембольше временитребуется длядобавленияновых элементовв нижнюю частьдерева. В наихудшемслучае, последобавленияN элементов, дерево будетиметь высотупорядка O(N).Полное времявставки всехэлементов вдерево будетпри этом порядкаO(N2).Поскольку дляобхода дереватребуется времяпорядка O(N), полное времясортировкичисел с использованиемдерева будетравно O(N2)+O(N)=O(N2).
Еслидерево остаетсядостаточнокоротким, оноимеет высотупорядка O(log(N)).В этом случаедля вставкиэлемента вдерево потребуетсявсего порядкаO(log(N))шагов. Вставкавсех N элементовв дерево потребуетпорядка O(N * log(N))шагов. Тогдасортировкаэлементов припомощи деревапотребуетвремени порядкаO(N * log(N)) + O(N) = O(N * log(N)).
Времявыполненияпорядка O(N * log(N))намного меньше, чем O(N2).Например, построениевысокого итонкого дерева, содержащего1000 элементов, потребуетвыполненияоколо миллионашагов. Построениекороткогодерева с высотойпорядка O(log(N))займет всегооколо 10.000 шагов.
Еслиэлементыпервоначальнорасположеныв случайномпорядке, формадерева будетпредставлятьчто то среднеемежду этимидвумя крайнимислучаями. Хотяего высотаможет оказатьсянесколькобольше, чемlog(N), оно, скорее всего, не будет слишкомтонким и высоким, поэтому алгоритмсортировкибудет выполнятьсядостаточнобыстро.

@Рис.6.21. Дерево, полученноедобавлениемэлементов впорядке 1, 6, 5, 2, 3, 4

==========140

В 7 главеописываютсяспособы балансировкидеревьев, длятого, чтобы онине становилисьслишком высокимии тонкими, независимоот того, в какомпорядке в нихдобавляютсяновые элементы.Тем не менее, эти методыдостаточносложны, и их неимеет смыслаприменять валгоритмесортировкипри помощидерева. Многиеиз алгоритмовсортировки, описанных в9 главе, болеепросты в реализациии обеспечиваютпри этом лучшуюпроизводительность.Деревьясо ссылками
Во 2 главепоказано, какдобавлениессылок к связнымспискам позволяетупростить выводэлементов вразном порядке.Вы можетеиспользоватьтот же подходдля упрощенияобращения кузлам деревав различномпорядке. Например, помещая ссылкив листья двоичногодерева, вы можетеоблегчитьвыполнениесимметричногои обратногообходов. Дляупорядоченногодерева, этообход в прямоми обратномпорядке сортировки.
Длясоздания ссылок, указатели напредыдущийи следующийузлы в порядкесимметричногообхода помещаютсяв неиспользуемыхуказателяхна дочерниеузлы. Если неиспользуетсяуказатель налевого потомка, то ссылказаписываетсяна его место, указывая напредыдущийузел при симметричномобходе. Еслине используетсяуказатель направого потомка, то ссылказаписываетсяна его место, указывая наследующий узелпри симметричномобходе. Посколькуссылки симметричны, и ссылки левыхпотомков указываютна предыдущие, а правых — наследующие узлы, этот тип деревьевназываетсядеревом ссимметричнымиссылками(symmetrically threadedtree). На рис.6.22 показано деревос симметричнымиссылками, которыеобозначеныпунктирнымилиниями.
Посколькуссылки занимаютместо указателейна дочерниеузлы дерева, нужно как торазличатьссылки и обычныеуказатели напотомков. Прощевсего добавитьк узлам новыепеременныеHasLeftChildи HasRightChildтипа Boolean, которые будутравны True, если узел имеетлевого илиправого потомкасоответственно.
Чтобыиспользоватьссылки дляпоиска предыдущегоузла, нужнопроверитьуказатель налевого потомкаузла. Если этотуказательявляется ссылкой, то ссылка указываетна предыдущийузел. Если значениеуказателя равноNothing, значит этопервый узелдерева, и поэтомуон не имеетпредшественников.В противномслучае, перейдемпо указателюк левому дочернемуузлу. Затемпроследуемпо указателямна правый дочернийузел потомков, до тех пор, покане достигнемузла, в которомна месте указателяна правогопотомка находитсяссылка. Этотузел (а не тот, на которыйуказываетссылка) являетсяпредшественникомисходного узла.Этот узел являетсясамым правымв левой от исходногоузла ветвидерева. Следующийкод демонстрируетпоиск предшественника:
    продолжение
--PAGE_BREAK--
@Рис.6.22. Дерево ссимметричнымиссылками

==========141

PrivateFunction Predecessor(node As ThreadedNode) As ThreadedNode Dim childAs ThreadedNode

Ifnode.LeftChild Is Nothing Then
'Это первый узелв порядкесимметричногообхода.
SetPredecessor = Nothing
ElseIf node.HasLeftChild Then
'Это указательна узел.
'Найти самыйправый узелв левой ветви.
Setchild = node.LeftChild
DoWhile child.HasRightChild
Setchild = child.RightChild
Loop
SetPredecessor = child
Else
'Ссылка указываетна предшественника.
SetPredecessor = node.LeftChild
EndIf
EndFunction

Аналогичновыполняетсяпоиск следующегоузла. Если указательна правый дочернийузел являетсяссылкой, то онауказывает наследующий узел.Если указательимеет значениеNothing, то это последнийузел дерева, поэтому он неимеет последователя.В противномслучае, переходимпо указателюк правому потомкуузла. Затемперемещаемсяпо указателямдочерних узловдо тех, пор, покаочереднойуказатель налевый дочернийузел не окажетсяссылкой. Тогданайденный узелбудет следующимза исходным.Это будет самыйлевый узел вправой от исходногоузла ветвидерева.
Удобнотакже ввестифункции длянахожденияпервого и последнегоузлов дерева.Чтобы найтипервый узел, просто проследуемпо указателямна левого потомкавниз от корнядо тех пор, покане достигнемузла, значениеуказателя налевого потомкадля которогоравно Nothing.Чтобы найтипоследний узел, проследуемпо указателямна правогопотомка внизот корня до техпор, пока недостигнем узла, значение указателяна правогопотомка длякоторого равноNothing.

PrivateFunction FirstNode() As ThreadedNode
Dimnode As ThreadedNode

Setnode = Root
DoWhile Not (node.LeftChild Is Nothing)
Setnode = node.LeftChild
Loop
SetPirstNode = node
EndFunction

PrivateFunction LastNode() As ThreadedNode
Dimnode As ThreadedNode
Setnode = Root
DoWhile Not (node.RightChild Is Nothing)
Setnode = node.RightChild
Loop
SetFirstNode = node
EndFunction

=========142

Припомощи этихфункций выможете легконаписать процедуры, которые выводятузлы деревав прямом илиобратном порядке:

PrivateSub Inorder()
Dimnode As ThreadedNode

'Найти первыйузел.
Setnode = FirstNode()

'Вывод списка.
DoWhile Not (node Is Nothing)
Printnode.Value
Setnode = Successor(node)
Loop
EndSub

PrivateSub PrintReverseInorder()
Dimnode As ThreadedNode

'Найти последнийузел
Setnode = LastNode

'Вывод списка.
DoWhile Not (node Is Nothing)
Printnode. Value
Setnode = Predecessor(node)
Loop
EndSub

Процедуравывода узловв порядкесимметричногообхода, приведеннаяранее в этойглаве, используетрекурсию. Дляустранениярекурсии выможете использоватьэти новые процедуры, которые неиспользуютни рекурсию, ни системныйстек.
Каждыйуказатель надочерние узлыв дереве содержитили указательна потомка, илиссылку напредшественникаили последователя.Так как каждыйузел имеет двауказателя надочерние узлы, то, если деревоимеет N узлов, то оно будетсодержать 2 * Nссылок и указателей.Эти алгоритмыобхода обращаютсяко всем ссылками указателямдерева одинраз, поэтомуони потребуютвыполненияO(2 * N) = O(N) шагов.
Можнонемного ускоритьвыполнениеэтих подпрограмм, если отслеживатьуказатели напервый и последнийузлы дерева.Тогда вам непонадобитсявыполнять поискпервого и последнегоузлов передтем, как вывестисписок узловпо порядку. Таккак при этомалгоритм обращаетсяко всем N узламдерева, времявыполненияэтого алгоритматакже будетпорядка O(N), нона практикеон будет выполнятьсянемного быстрее.

========143
Работас деревьямисо ссылками
Дляработы с деревомсо ссылками, нужно, чтобыможно былодобавлять иудалять узлыиз дерева, сохраняяпри этом егоструктуру.
Предположим, что требуетсядобавить новоголевого потомкаузла A. Так какэто место незанято, то наместе указателяна левого потомкаузла A находитсяссылка, котораяуказывает напредшественникаузла A. Посколькуновый узелзаймет местолевого потомкаузла A, он станетпредшественникомузла A. Узел A будетпоследователемнового узла.Узел, которыйбыл предшественникомузла A до этого, теперь становитсяпредшественникомнового узла.На рис. 6.23 показанодерево с рис.6.22 после добавлениянового узлаX в качествелевого потомкаузла H.
Еслиотслеживатьуказатель напервый и последнийузлы дерева, то требуетсятакже проверить, не являетсяли теперь новыйузел первымузлом дерева.Если ссылкана предшественникадля нового узлаимеет значениеNothing, то это новыйпервый узелдерева.

@Рис.6.23. Добавлениеузла X к деревусо ссылками

=========144

Учитываявсе вышеизложенное, легко написатьпроцедуру, которая добавляетнового левогопотомка к узлу.Вставка правогопотомка выполняетсяаналогично.

PrivateSub AddLeftChild(parent As ThreadedNode, child As ThreadedNode)
'Предшественникродителя становитсяпредшественникомнового узла.
Setchild. LeftChild = parent.LeftChild
child.HasLeftChild= False

'Вставитьузел.
Setparent.LeftChild = child
parent.HasLeftChild= True

'Родитель являетсяпоследователемнового узла.
Setchild.RightChild = parent
child.HasRightChild= False

'Определить, является линовый узелпервым узломдерева.
Ifchild.LeftChild Is Nothing Then Set FirstNode = child
EndSub

Передтем, как удалитьузел из дерева, необходимовначале удалитьвсех его потомков.После этоголегко удалитьуже сам узел.
Предположим, что удаляемыйузел являетсялевым потомкомсвоего родителя.Его указательна левого потомкаявляется ссылкой, указывающейна предыдущийузел в дереве.После удаленияузла, его предшественникстановитсяпредшественникомродителя удаленногоузла. Чтобыудалить узел, просто заменяемуказатель налевого потомкаего родителяна указательна левого потомкаудаляемогоузла.
Указательна правогопотомка удаляемогоузла являетсяссылкой, котораяуказывает наследующий узелв дереве. Таккак удаляемыйузел являетсялевым потомкомсвоего родителя, и посколькуу него нет потомков, эта ссылкауказывает народителя, поэтомуее можно простоопустить. Нарис. 6.24 показанодерево с рис.6.23 после удаленияузла F. Аналогичноудаляетсяправый потомок.

PrivateSub RemoveLeftChild(parent As ThreadedNode)
Dimtarget As ThreadedNode

Settarget = parent.LeftChild
Setparent.LeftChild = target.LeftChild
EndSub

@Рис.6.24. Удалениеузла F из деревасо ссылками

=========145
Квадродеревья
Квадродеревья(quadtrees) описываютпространственныеотношения междуэлементамина площади.Например, этоможет бытькарта, а элементымогут представлятьсобой положениедомов или предприятийна ней.
Каждыйузел квадродеревапредставляетсобой участокна площади, представленнойквадродеревом.Каждый узел, кроме листьев, имеет четырепотомка, которыепредставляютчетыре квадранта.Листья могутхранить своиэлементы вколлекцияхсвязных списков.Следующий кодпоказываетсекцию Declarationsдля классаQtreeNode.

'Потомки.
PublicNWchild As QtreeNode
PublicNEchild As QtreeNode
PublicSWchild As QtreeNode
PublicSEchild As QtreeNode

'Элементы узла, если это нелист.
PublicItems As New Collection

Элементы, записанныев квадродереве, могут содержатьпространственныеданные любоготипа. Они могутсодержатьинформациюо положении, которую деревоможет использоватьдля поискаэлементов.Переменныев простом классеQtreeItem, который представляетэлементы, состоящиеиз точек наместности, определяютсятак:

PublicX As Single
PublicY As Single

Чтобыпостроитьквадродерево, вначале поместимвсе элементыв корневойузел. Затемопределим, содержит лиэтот узел достаточномного элементов, чтобы его стоилоразделить нанесколькоузлов. Если этотак, создадимчетыре потомкаузла и распределимэлементы междучетырьмя потомкамив соответствиис их положениемв четырех квадрантахисходной области.Затем рекурсивнопроверяем, ненужно ли разбитьна несколькоузлов дочерниеузлы. Продолжимразбиение дотех пор, покавсе листья небудут содержатьне больше некоторогозаданного числаэлементов.
На рис.6.25 показанонесколькоэлементовданных, расположенныхв виде квадродерева.Каждая областьразбиваетсядо тех пор, покаона не будетсодержать неболее двухэлементов.
Квадродеревьяудобно применятьдля поискаблизлежащихобъектов.Предположим, имеется программа, которая рисуеткарту с большимчислом населенныхпунктов. Послетого, как пользовательщелкнет мышьюпо карте, программадолжна найтиближайший квыбранной точкенаселенныйпункт. Программаможет перебратьвесь списокнаселенныхпунктов, проверяядля каждогоего расстояниеот заданнойточки. Если всписке N элементов, то сложностьэтого алгоритмапорядка O(N).

====146

@Рис.6.25. Квадродерево

Этуоперацию можновыполнитьнамного быстреепри помощиквадродерева.Начнем с корневогоузла. При каждойпроверке квадродереваопределяем, какой из квадрантовсодержит точку, которую выбралпользователь.Затем спустимсявниз по деревук соответствующемудочернему узлу.Если пользовательвыбрал верхнийправый уголобласти узла, нужно спуститьсяк северо восточномупотомку. Продолжимдвижение внизпо дереву, покане дойдем долиста, которыйсодержит выбраннуюпользователемточку.
ФункцияLocateLeafкласса QtreeNodeиспользуетэтот подходдля поискалиста дерева, который содержитвыбраннуюточку. Программаможет вызватьэту функциюв строке Setthe_leaf = Root.LocateLeaf(X,Y,Gxmin,Gxmax,Gymax), где Gxmin,Gxmax,Gymin,Gymax —это границыпредставленнойдеревом области.

PublicFunction LocateLeaf (X As Single, Y As Single, _
xminAs Single, xmax As Single, ymin As Single, ymax As Single)_
AsQtreeNode

Dimxmid As Single
Dimymid As Single
Dimnode As QtreeNode

IfNWchild Is Nothing Then
'Узел не имеетпотомков. Искомыйузел найден.
SetLocateLeaf = Me
ExitFunction
EndIf

'Найти соответстующегопотомка.
xmid= (xmax + xmin) / 2
ymid= (ymax + ymin) / 2
IfX
IfY
SetLocateLeaf = NWchild.LocateLeaf( _
X,Y, xmin, xmid, ymin, ymid)
Else
SetLocateLeaf = SWchild.LocateLeaf _
X,Y, xmin, xmid, ymid, ymax)
EndIf
Else
IfY
SetLocateLeaf = NEchild.LocateLeaf( _
X,Y, xmid, xmax, ymin,ymid)
Else
SetLocateLeaf = SEchild.LocateLeaf( _
X,Y, xmid, xmax, ymid, ymax)
EndIf
EndIf
EndFunction

Посленахождениялиста, которыйсодержит точку, проверяемнаселенныепункты в листе, чтобы найти, который из нихближе всегоот выбраннойточки. Это делаетсяпри помощипроцедурыNearPointInLeaf.

PublicSub NearPointInLeaf (X As Single, Y As Single, _
best_itemAs QtreeItem,best_dist As Single, comparisons As Long)

Dimnew_item As QtreeItem
DimDx As Single
DimDy As Single
Dimnew_dist As Single

'Начнем с заведомоплохого решения.
best_dist= 10000000
Setbest_item = Nothing

'Остановитьсяесли лист несодержит элементов.
IfItems.Count

ForEach new_item In Items
comparisons= comparisons + 1
Dx= new_item.X — X
Dy= new_item.Y — Y
new_dist=Dx * Dx + Dy * Dy
Ifbest_dist > new_dist Then
best_dist= new_dist
Setbest_item = new_item
EndIf
Nextnew_item
EndSub

======147-148

Элемент, который находитпроцедураNearPointLeaf, обычно и естьэлемент, которыйпользовательпытался выбрать.Тем не менее, если элементнаходитсявблизи границымежду двумяузлами, можетоказаться, чтоближайший квыбранной точкеэлемент находитсяв другом узле.
Предположим, что Dmin —это расстояниемежду выбраннойпользователемточкой и ближайшимиз найденныхдо сих пор населенныхпунктов. ЕслиDminменьше, чемрасстояниеот выбраннойточки до краялиста, то поискзакончен. Населенныйпункт находитсяпри этом слишкомдалеко от краялиста, чтобыв каком либодругом листемог существоватьпункт, расположенныйближе к заданнойточке.
В противномслучае нужноснова начатьс корня и двигатьсяпо дереву, проверяявсе узлы квадродеревьев, которые находятсяна расстояниименьше, чемDminот заданнойточки. Еслинайдутся элементы, которые расположеныближе, изменимзначение Dminи продолжимпоиск. Послезавершенияпроверки ближайшихк точке листьев, нужный элементбудет найден.ПодпрограммаCheckNearByLeavesиспользуетэтот подходдля завершенияпоиска.

PublicSub CheckNearbyLeaves(exclude As QtreeNode, _
XAs Single, Y As Single, best_item As QtreeItem,_
best_distAs Single, comparisons As Long, _
xminAs Single, xmax As Single, yminAs Single, ymax As Single)

Dimxmid As Single
Dimymid As Single
Dimnew_dist As Single
Dimnew_item As QtreeItem

'Если это лист, который мыдолжны исключить,
'ничего не делать.
IfMe Is exclude Then Exit Sub

'Если это лист, проверить его.
IfSWchild Is Nothing Then
NearPointInLeafX, Y, new_item, new_dist, comparisons
Ifbest_dist > new_dist Then
best_dist= new_dist
Setbest_item = new_item
EndIf
ExitSub
EndIf

'Найти потомков, которые удаленыне больше, чемна best_dist
'от выбраннойточки.
xmid= (xmax + xmin) / 2
ymid= (ymax + ymin) / 2
IfX — Sqr(best_dist)

'Продолжаемс потомкамина западе.
IfY — Sqr(best_dist)
'Проверитьсеверо-западногопотомка.
NWchild.CheckNearbyLeaves_
exclude,X, Y, best_item, _
best_dist,comparisons, _
xmin,xmid, ymin, ymid
EndIf
IfY + Sqr(best_dist) > ymid Then
'Проверитьюго-западногопотомка.
SWchiId.CheckNearbyLeaves_
exclude,X, Y, best_item, _
best_dist,comparisons, _
xmin,xmid, ymid, ymax
EndIf
EndIf
IfX + Sqr(best_dist) > xmid Then
'Продолжитьс потомкамина востоке.
IfY — Sqr(best_dist)
'Проверитьсеверо-восточногопотомка.
NEchild.CheckNearbyLeaves_
exclude,X, Y, best_item, _
best_dist,comparisons, _
xmid,xmax, ymin, ymid
EndIf
IfY + Sqr(best_dist) > ymid Then
'Проверитьюговосточногопотомка.
SEchild.CheckNearbyLeaves_
exclude,X, Y, best_item, _
best_dist,comparisons, _
xmid,xmax, ymid, ymax
EndIf
EndIf
EndSub

=====149-150

ПодпрограммаFindPointиспользуетподпрограммыLocateLeaf,NearPointInLeaf, и CheckNearbyLeaves, из классаQtreeNodeдля быстрогопоискаэлементав квадродереве.

FunctionFindPoint(X As Single, Y As Single, comparisons As Long) _As QtreeItem

Dimleaf As QtreeNode
Dimbest_item As QtreeItem
Dimbest_dist As Single

'Определить, в каком листенаходитсяточка.
Setleaf = Root.LocateLeaf( _
X,Y, Gxmin, Gxmax, Gymin, Gymax)

'Найти ближайшуюточку в листе.
leaf.NearPointInLeaf_
X,Y, best_item, best_dist, comparisons

'Проверитьсоседние листья.
Root.CheckNearbyLeaves_
leaf,X, Y, best_item, best_dist, _
comparisons,Gxmin, Gxmax, Gymin, Gymax

SetFindPoint = best_item
EndFunction

ПрограммаQtreeиспользуетквадродерево.При стартепрограммазапрашиваетчисло элементовданных, котороеона должнасоздать, затемона создаетэлементы ирисует их ввиде точек.Задавайтевначале небольшое(около 1000) числоэлементов, покавы не определите, насколькобыстро вашкомпьютер можетсоздаватьэлементы.
Интереснонаблюдатьквадродеревья, элементы которыхраспределенынеравномерно, поэтому программавыбирает точкипри помощифункции странногоаттрактора(strange attractor)из теории хаоса(chaos theory).Хотя кажется, что элементыследуют в случайномпорядке, ониобразуют интересныекластеры.
Привыборе какой либоточки на формепри помощимыши, программаQtreeнаходит ближайшийк ней элемент.Она подсвечиваетэтот элементи выводит числопроверенныхпри его поискеэлементов.
В менюOptions (Опции)программы можнозадать, должнали программаиспользоватьквадродеревьяили нет. Еслипоставитьгалочку в пунктеUse Quadtree(Использоватьквадродерево), то программавыводит наэкран квадродеревои используетего для поискаэлементов. Еслиэтот пункт невыбран, программане отображаетквадродеревои находит нужныеэлементы путемперебора.
Программапроверяетнамного меньшеечисло элементови работаетнамного быстреепри использованииквадродерева.Если этот эффектне слишкомзаметен навашем компьютере, запуститепрограмму, задав при старте10.000 или 20.000 входныхэлементов. Вызаметите разницудаже на компьютерес процессоромPentium с тактовойчастотой 90 МГц.
На рис.6.26 показано окнопрограмма Qtreeна которомизображено10.000 элементов.Маленькийпрямоугольникв верхнем правомуглу обозначаетвыбранныйэлемент. Меткав верхнем левомуглу показывает, что программапроверила всего40 из 10.000 элементовперед тем, какнайти нужный.ИзменениеMAX_PER_NODE
Интереснопоэкспериментироватьс программойQtree, изменяя значениеMAX_PER_NODE, определенноев разделеDeclarationsкласса QtreeNode.Это максимальноечисло элементов, которые могутпоместитьсяв узле квадродеревабез его разбиения.Программаобычно используетзначениеMAX_PER_NODE = 100.
    продолжение
--PAGE_BREAK--
======151

@Рис.6.26. ПрограммаQtree

Есливы уменьшитеэто число, например, до 10, то в каждомузле будетнаходитьсяменьше элементов, поэтому программабудет проверятьменьше элементов, чтобы найтиближайший квыбранной вамиточке. Поискбудет выполнятьсябыстрее. С другойстороны, программепридется создатьнамного большеузлов квадродерева, поэтому оназаймет большепамяти.
Наоборот, если вы увеличитеMAX_PER_NODEдо 1000, программасоздаст намногоменьше узлов.При этом потребуетсябольше временина поиск элементов, но дерево будетменьше, и займетменьше памяти.
Этопример компромиссамежду временеми пространством.Использованиебольшего числаузлов квадродереваускоряет поиск, но занимаетбольше памяти.В этом примере, при значениипеременнойMAX_PER_NODEпримерно равном100, достигаетсяравновесиемежду скоростьюи использованиемпамяти. Длядругих приложенийвам можетпотребоватьсяпоэкспериментироватьс различнымизначениямипеременнойMAX_PER_NODE, чтобы найтиоптимальное.Использованиепсевдоуказателейв квадродеревьях
ПрограммаQtreeиспользуетбольшое числоклассов и коллекций.Каждый внутреннийузел квадродеревасодержит четырессылки на дочерниеузлы. Листьявключают большиеколлекции, вкоторых находятсяэлементы узла.Все эти объектыи коллекциизамедляютработу программы, если она содержитбольшое числеэлементов.Создание объектовотнимает многовремени и памяти.Если программасоздает множествообъектов, онаможет начатьобращатьсяк файлу подкачки, что сильнозамедлит ееработу.
К сожалению, выигрыш отиспользованияквадродеревьевбудет максимальным, если программасодержит многоэлементов.Чтобы улучшитьпроизводительностьбольших приложений, вы можетеиспользоватьметоды работыс псевдоуказателями, описанные во2 главе.

=====152

ПрограммаQtree2создает квадродеревопри помощипсевдоуказателей.Узлы и элементынаходятся вмассивах определенныхпользователемструктур данных.В качествеуказателей, эта программаиспользуетиндексы массивоввместо ссылокна объекты. Водном из тестовна компьютерес процессоромPentium с тактовойчастотой 90 МГц, программе Qtreeпотребовалось25 секунд дляпостроенияквадродерева, содержащего30.000 элементов.ПрограммеQtree2понадобилосьвсего 3 секундыдля созданиятого же дерева.Восьмеричныедеревья
Восьмеричныедеревья (octtrees)аналогичныквадродеревьям, но они разбиваютобласть недвумерного, а трехмерногопространства.Восьмеричныедеревья содержатне четыре потомка, как квадродеревья, а восемь, разбиваяобъем областина восемь частей —верхнюю северо западную, нижнюю северо западную, верхнюю северо восточную, нижнюю северо восточнуюи так далее.
Восьмеричныедеревья полезныпри работе собъектами, расположеннымив пространстве.Например, роботможет использоватьвосьмеричноедерево дляотслеживанияблизлежащихобъектов. Программарейтрейсингаможет использоватьвосьмеричноедерево длятого, чтобыбыстро оценить, проходит лилуч поблизостиот объектаперед тем, какначать медленныйпроцесс вычисленийточного пересеченияобъекта и луча.
Восьмеричныедеревья можностроить, используяпримерно теже методы, чтои для квадродеревьев.Резюме
Существуетмножествоспособовпредставлениядеревьев. Наиболееэффективными компактнымиз них являетсязапись полныхдеревьев вмассивах.Представлениедеревьев в видеколлекцийдочерних узловупрощает работус ними, но приэтом программавыполняетсямедленнее ииспользуетбольше памяти.Представлениенумерациейсвязей позволяетбыстро выполнятьобход дереваи используетменьше памяти, чем коллекциипотомков, ноего сложномодифицировать.Тем не менее, его важнопредставлять, потому что оночасто используетсяв сетевых алгоритмах.

=====153
Глава7. Сбалансированныедеревья
Приработе с упорядоченнымдеревом, вставкеи удаленииузлов, деревоможет статьнесбалансированным.Когда это происходит, то алгоритмы, работы с деревомстановятсяменее эффективными.Если деревостановитсясильно несбалансированным, оно практическипредставляетвсего лишьсложную формусвязного списка, и программа, использующаятакое дерево, может иметьочень низкуюпроизводительность.
В этойглаве обсуждаютсяметоды, которыеможно использоватьдля балансировкидеревьев, дажеесли узлы удаляютсяи добавляютсяс течениемвремени. Балансировкадерева позволяетему оставатьсяпри этом достаточноэффективным.
Главаначинаетсяс описаниятого, что понимаетсяпод несбалансированнымдеревом идемонстрацииухудшенияпроизводительностидля несбалансированныхдеревьев. Затемв ней обсуждаютсяАВЛ деревья, высота левогои правого поддеревьевв каждом узлекоторых отличаетсяне больше, чемна единицу.Сохраняя этосвойствоАВЛ деревьев, можно поддерживатьтакое деревосбалансированным.
Затемв главе описываютсяБ деревья иБ+деревья, вкоторых вселистья имеютодинаковуюглубину. Есличисло ветвей, выходящих изкаждого узланаходится вопределенныхпределах, такиедеревья остаютсясбалансированными.Б деревья иБ+деревья обычноиспользуютсяпри программированиибаз данных.Последняяпрограмма, описанная вэтой главе, используетБ+дерево дляреализациипростой, нодостаточномощной базыданных.Сбалансированностьдерева
Какупоминалосьв 6 главе, формаупорядоченногодерева зависитот порядкавставки в негоновых узлов.На рис. 7.1 показанодва различныхдерева, созданныхпри добавленииодних и тех жеэлементов вразном порядке.
Высокиеи тонкие деревья, такие как левоедерево на рис.7.1, могут иметьглубину порядкаO(N). Вставка илипоиск элементав таком несбалансированномдереве можетзанимать порядкаO(N) шагов. Дажеесли новыеэлементы вставляютсяв дерево в случайномпорядке, в среднемони дадут деревос глубинойN / 2, что такжепорядка O(N).
Предположим, что строитсяупорядоченноедвоичное дерево, содержащее1000 узлов. Еслидерево сбалансировано, то высота деревабудет порядкаlog2(1000), или примерноравна 10. Вставканового элементав дерево займетвсего 10 шагов.Если деревовысокое и тонкое, оно может иметьвысоту 1000. В этомслучае, вставкаэлемента вконец деревазаймет 1000 шагов.

======155

@Рис.7.1. Деревья, построенныев различномпорядке

Предположимтеперь, что мыхотим добавитьк дереву еще1000 узлов. Еслидерево остаетсясбалансированным, то все 1000 узловпоместятсяна следующемуровне дерева.При этом длявставки новыхэлементовпотребуетсяоколо 10 * 1000 = 10.000шагов. Еслидерево былоне сбалансированои остаетсятаким в процессероста, то привставке каждогонового элементаоно будет становитьсявсе выше. Вставкаэлементов приэтом потребуетпорядка 1000 + 1001+ … +2000 = 1,5 миллионашагов.
Хотянельзя бытьуверенным, чтоэлементы будутдобавлятьсяи удалятьсяиз дерева внужном порядке, можно использоватьметоды, которыебудут поддерживатьсбалансированностьдерева, независимоот порядкавставки илиудаления элементов.АВЛ деревья
АВЛ деревья(AVL trees) былиназваны в честьрусских математиковАдельсона Вельскогои Лэндиса, которыеих изобрели.Для каждогоузла АВЛ дерева, высота левогои правого поддеревьевотличаетсяне больше, чемна единицу. Нарис. 7.2 показанонесколькоАВЛ деревьев.
ХотяАВЛ деревоможет бытьнесколько выше, чем полноедерево с темже числом узлов, оно также имеетвысоту порядкаO(log(N)). Это означает, что поиск узлав АВЛ деревезанимает времяпорядка O(log(N)), что достаточнобыстро. Не стольочевидно, чтоможно вставитьили удалитьэлемент изАВЛ дереваза время порядкаO(log(N)), сохраняяпри этом порядокдерева.

======156

@Рис.7.2. АВЛ деревья

Процедура, которая вставляетв дерево новыйузел, рекурсивноспускаетсявниз по дереву, чтобы найтиместоположениеузла. Послевставки элемента, происходятвозвраты изрекурсивныхвызовов процедурыи обратныйпроход вверхпо дереву. Прикаждом возвратеиз процедуры, она проверяет, сохраняетсяли все еще свойствоАВЛ деревьевна верхнемуровне. Этоттип обратнойрекурсии, когдапроцедуравыполняетважные действияпри выходе изцепочки рекурсивныхвызовов, называетсявосходящей(bottom up)рекурсией.
Приобратном проходевверх по дереву, процедура такжепроверяет, неизмениласьли высота поддерева, с которым онаработает. Еслипроцедурадоходит доточки, в которойвысота поддереване изменилась, то высота следующихподдеревьевтакже не моглаизмениться.В этом случае, снова требуетсябалансировкадерева, и процедураможет закончитьпроверку.
Например, дерево слевана рис. 7.3 являетсясбалансированнымАВЛ деревом.Если добавитьк дереву новыйузел E, то получитсясреднее деревона рисунке.Затем выполняетсяпроход вверхпо дереву отнового узлаE. В самом узлеE дерево сбалансировано, так как оба егоподдеревапустые и имеютодинаковуювысоту 0.
В узлеD дерево такжесбалансировано, так как еголевое поддеревопустое, и имеетпоэтому высоту0. Правое поддеревосодержит единственныйузел E, и поэтомуего высотаравна 1. Высотыподдеревьевотличаютсяне больше, чемна единицу, поэтому деревосбалансированов узле D.
В узлеC дерево уже несбалансировано.Левое поддеревоузла C имеетвысоту 0, а правое —высоту 2. Этиподдеревьяможно сбалансировать, как показанона рис. 7.3 справа, при этом узелC заменяетсяузлом D. Теперьподдерево скорнем в узлеD содержит узлыC, D и E, и имеет высоту2. Заметьте, чтовысота поддеревас корнем в узлеC, которое ранеенаходилосьв этом месте, также быларавна 2 до вставкинового узла.Так как высотаподдерева неизменилась, то дерево такжеокажетсясбалансированнымво всех узлахвыше D.ВращенияАВЛ деревьев
Привставке узлав АВЛ дерево, в зависимостиот того, в какуючасть деревадобавляетсяузел, существуетчетыре вариантабалансировки.Эти способыназываютсяправым и левымвращением, ивращениемвлево вправои вправо влево, и обозначаютсяR, L, LR и RL.
Предположим, что в АВЛ деревовставляетсяновый узел, итеперь деревостановитсянесбалансированнымв узле X, какпоказано нарис. 7.4. На рисункеизображенытолько узелX и два его дочернихузла, а остальныечасти дереваобозначенытреугольниками, так как их нетребуетсярассматриватьподробно.
Новыйузел может бытьвставлен влюбое из четырехподдеревьевузла X, изображенныхв виде треугольников.Если вы вставляетеузел в одно изэтих поддеревьев, то для балансировкидерева потребуетсявыполнитьсоответствующеевращение. Помните, что иногдабалансировкане нужна, есливставка новогоузла не нарушаетупорядоченностьдерева.Правоевращение
Вначалепредположим, что новый узелвставляетсяв поддеревоR на рис. 7.4. В этомслучае не нужноизменять дваправых поддереваузла X, поэтомуих можно объединить, изобразив однимтреугольником, как показанона рис. 7.5. Новыйузел вставляетсяв дерево T1, приэтом поддеревоTA с корнем вузле A становитсяне менее, чемна два уровнявыше, чем поддеревоT3.
На самомделе, посколькудо вставкинового узладерево былоАВЛ деревом, то TA должнобыло быть вышеподдерева T3 небольше, чем наодин уровень.После вставкиодного узлаTA должно бытьвыше поддереваT3 ровно на двауровня.
Такжеизвестно, чтоподдерево T1выше поддереваT2 не больше, чем на одинуровень. Иначеузел X не былбы самым нижнимузлом с несбалансированнымиподдеревьями.Если бы T1 былона два уровнявыше, чем T2, тодерево былобы несбалансированнымв узле A.

@Рис.7.4. Анализ несбалансированногоАВЛ дерева

========158

@Рис.7.5. Вставка новогоузла в поддеревоR

В этомслучае, можнопереупорядочитьузлы при помощиправого вращения(right rotation), как показанона рис. 7.6. Этовращение называетсяправым, так какузлы A и X как бывращаютсявправо.
Заметим, что это вращениесохраняетпорядок «меньше»расположенияузлов дерева.При симметричномобходе любогоиз таких деревьевобращение ковсем поддеревьями узлам деревапроисходитв порядке T1,A, T2, X, T3. Посколькусимметричныйобход обоихдеревьев происходитодинаково, тои порядокрасположенияэлементов вних будет одинаковым.
Важнотакже заметить, что высотаподдерева, скоторым мыработаем, остаетсянеизменной.Перед тем, какбыл вставленновый узел, высота поддеревабыла равнавысоте поддереваT2 плюс 2. Послевставки узлаи выполненияправого вращения, высота поддереватакже остаетсяравной высотеподдерева T2плюс 2. Все частидерева, лежащиениже узла X приэтом такжеостаютсясбалансированными, поэтому нетребуетсяпродолжатьбалансировкудерева дальше.Левоевращение
Левоевращение (leftrotation) выполняетсяаналогичноправому. Оноиспользуется, если новый узелвставляетсяв поддеревоL, показанноена рис. 7.4. На рис.7.7 показаноАВЛ дереводо и после левоговращения.

@Рис.7.6. Правое вращение

========159

@Рис.7.7. До и послелевого вращения
Вращениевлево вправо
Еслиузел вставляетсяв поддеревоLR, показанноена рис. 7.4, нужнорассмотретьеще один нижележащийуровень. Нарис. 7.8. показанодерево, в которомновый узелвставляетсяв левую частьT2 поддереваLR. Так же легкоможно вставитьузел в правоеподдерево T3.В обоих случаях, поддеревьяTA и TC останутсяАВЛ поддеревьями, но поддеревоTX уже не будеттаковым.
Таккак дерево довставки узлабыло АВЛ деревом, то TA было вышеT4 не больше, чем на одинуровень. Посколькудобавлен толькоодин узел, тоTA вырастеттолько на одинуровень. Этозначит, что TAтеперь будетточно на двауровня вышеT4.
Такжеизвестно, чтоподдерево T2не более, чемна один уровеньвыше, чем T3.Иначе TC не былобы сбалансированным, и узел X не былбы самым нижнимв дереве узломс несбалансированнымиподдеревьями.
ПоддеревоT1 должно иметьту же глубину, что и T3. Еслибы оно былокороче, то поддеревоTA было бы несбалансировано, что сновапротиворечитпредположениюо том, что узелX — самый нижнийузел в дереве, имеющий несбалансированныеподдеревья.Если бы поддеревоT1 имело большуюглубину, чемT3, то глубинаподдерева T1была бы на 2 уровнябольше, чемглубина поддереваT4. В этом случаедерево былобы несбалансированнымдо вставки внего новогоузла.
Всеэто означает, что нижниечасти деревьеввыглядят вточности так, как показанона рис. 7.8. ПоддеревоT2 имеет наибольшуюглубину, глубинаT1 и T3 на одинуровень меньше, а T4 расположеноеще на одинуровень выше, чем T3 и T3.

@Рис.7.8. Вставка новогоузла в поддеревоLR

==========160

@Рис.7.9. Вращениевлево вправо

Используяэти факты, можносбалансироватьдерево, какпоказано нарис. 7.9. Это называетсявращениемвлево вправо(left rightrotation), так какпри этом вначалеузлы A и C как бывращаютсявлево, а затемузлы C и X вращаютсявправо.
Как идругие вращения, вращение этоготипа не изменяетпорядок элементовв дереве. Присимметричномобходе деревадо и после вращенияобращение кузлам и поддеревьямпроисходитв порядке: T1,A, T2, C, T3, X, T4.
Высотадерево послебалансировкитакже не меняется.До вставкинового узла, правое поддеревоимело высотуподдерева T1плюс 2. Послебалансировкидерева, высотаэтого поддереваснова будетравна высотеT1 плюс 2. Этозначит, чтоостальная частьдерева такжеостаетсясбалансированной, и нет необходимостипродолжатьбалансировкудальше.Вращениевправо влево
Вращениевправо влево(right leftrotation) аналогичновращению влево вправо(). Оно используетсядля балансировкидерева послевставки узлав поддеревоRL на рис. 7.4. На рис.7.10 показаноАВЛ дереводо и после вращениявправо влево.Резюме
На рис.7.11 показаны всевозможныевращения АВЛ дерева.Все они сохраняютпорядок симметричногообхода дерева, и высота деревапри этом всегдаостается неизменной.После вставкинового элементаи выполнениясоответствующеговращения, деревоснова оказываетсясбалансированным.Вставкаузлов на языкеVisual Basic
Передтем, как перейтик обсуждениюудаления узловиз АВЛ деревьев, в этом разделеобсуждаютсянекоторыедетали реализациивставки узлав АВЛ деревона языке VisualBasic.
Кромеобычных полейLeftChildи RightChild, класс AVLNodeсодержит такжеполе Balance, которое указывает, которое изподдеревьевузла выше. Егозначение равно-1, если левоеподдерево выше,1 — если вышеправое, и 0 —если оба поддереваимеют одинаковуювысоту.

======161

@Рис.7.10. До и послевращения вправо влево

PublicLeftChild As AVLNode
PublicRightChild As AVLNode
PublicBalance As Integer

Чтобысделать кодболее простымдля чтения, можно использоватьпостоянныеLEFT_HEAVY,RIGHT_HEAVY, и BALANCEDдля представленияэтих значений.

GlobalConst LEFT_HEAVY = -1
GlobalConst BALANCED = 0
GlobalConst RIGHT_HEAVY = 1

ПроцедураInsertItem, представленнаяниже, рекурсивноспускаетсявниз по деревув поиске новогоместоположенияэлемента. Когдаона доходитдо нижнегоуровня дерева, она создаетновый узел ивставляет егов дерево.
ЗатемпроцедураInsertItemиспользуетвосходящуюрекурсию длябалансировкидерева. Привыходе из рекурсивныхвызовов процедуры, она движетсяназад по дереву.При каждомвозврате изпроцедуры, онаустанавливаетпараметр has_grown, чтобы определить, увеличиласьли высота поддерева, которое онапокидает. ВэкземплярепроцедурыInsertItem, который вызвалэтот рекурсивныйвызов, процедураиспользуетэтот параметрдля определениятого, являетсяли проверяемоедерево несбалансированным.Если это так, то процедураприменяет длябалансировкидерева соответствующеевращение.
Предположим, что процедурав настоящиймомент обращаетсяк узлу X. Допустим, что она передэтим обращаласьк правому поддеревуснизу от узлаX и что параметрhas_grownравен true, означая, чтоправое поддеревоувеличилось.Если поддеревьяузла X до этогоимели одинаковуювысоту, тогдаправое поддеревостанет теперьвыше левого.В этой точкедерево сбалансировано, но поддеревос корнем в узлеX выросло, таккак вырослоего правоеподдерево.
Еслилевое поддеревоузла X вначалебыло выше, чемправое, то левоеи правое поддеревьятеперь будутиметь одинаковуювысоту. Высотаподдерева скорнем в узлеX не изменилась —она по прежнемуравна высотелевого поддереваплюс 1. В этомслучае процедураInsertItemустановитзначение переменнойhas_grownравным false, показывая, чтодерево сбалансировано.
    продолжение
--PAGE_BREAK--
========162

@Рис.7.11 Различныевращения АВЛ дерева

======163

В концеконцов, еслиправое поддеревоузла X былопервоначальновыше левого, то вставканового узладелает деревонесбалансированнымв узле X. ПроцедураInsertItemвызывает подпрограммуRebalanceRigthGrewдля балансировкидерева. ПроцедураRebalanceRigthGrewвыполняет левоевращение иливращениевправо влево, в зависимостиот ситуации.
Еслиновый элементвставляетсяв левое поддерево, то подпрограммаInsertItemвыполняетаналогичнуюпроцедуру.

PublicSub InsertItem(node As AVLNode, parent AsAVLNode, _
txtAs String, has_grown As Boolean)
Dimchild As AVLNode

'Если это нижнийуровень дерева, поместить
'в родителяуказатель нановый узел.
Ifparent Is Nothing Then
Setparent = node
parent.Balance= BALANCED
has_grown= True
ExitSub
EndIf

'Продолжитьс левым и правымподдеревьями.
Iftxt
'Вставить потомкав левое поддерево.
Setchild = parent.LeftChild
InsertItemnode, child, txt, has_grown
Setparent.LeftChild = child

'Проверить, нужна ли балансировка.Она будет
'не нужна, есливставка узлане нарушила
'балансировкудерева или оноуже было сбалансировано
'на более глубокомуровне рекурсии.В любом случае
'значение переменнойhas_grown будет равноFalse.
IfNot has_grown Then Exit Sub

Ifparent.Balance = RIGHT_HEAVY Then
'Перевешивалаправая ветвь, теперь баланс
'восстановлен.Это поддеревоне выросло,
'поэтому деревосбалансировано.
parent.Balance= BALANCED
has_grown= False
ElseIfparent.Balance = BALANCED Then
'Было сбалансировано, теперь перевешиваетлевая ветвь.
'Поддерево всееще сбалансировано, но оно выросло,
'поэтому необходимопродолжитьпроверку дерева.
parent.Balance= LEFT_HEAVY
Else
'Перевешивалалевая ветвь, осталосьнесбалансировано.
'Выполнитьвращение длябалансировкина уровне
'этого узла.
RebalanceLeftGrewparent
has_grown= False
EndIf ' Закончитьпроверку балансировкиэтого узла.
Else
'Вставить потомкав правое поддерево.
Setchild = parent.RightChild
InsertItemnode, child, txt, has_grown
Setparent.RightChild = child

'Проверить, нужна ли балансировка.Она будет
'не нужна, есливставка узлане нарушила
'балансировкудерева или оноуже было сбалансировано
'на более глубокомуровне рекурсии.В любом случае
'значение переменнойhas_grown будет равноFalse.
IfNot has_grown Then Exit Sub

Ifparent.Balance = LEFT_HEAVY Then
'Перевешивалалевая ветвь, теперь баланс
'восстановлен.Это поддеревоне выросло,
'поэтому деревосбалансировано.
parent.Balance= BALANCED
has_grown= False
ElseIfparent.Balance = BALANCED Then
'Было сбалансировано, теперь перевешиваетправая
'ветвь. Поддеревовсе еще сбалансировано,
'но оно выросло, поэтому необходимопродолжить
'проверку дерева.
parent.Balance= RIGHT_HEAVY
Else
'Перевешивалаправая ветвь, осталосьнесбалансировано.
'Выполнитьвращение длябалансировкина уровне
'этого узла.
RebalanceRightGrewparent
has_grown= False
EndIf ' Закончитьпроверку балансировкиэтого узла.
EndIf ' Endif длялевого поддереваelse правоеподдерево.
EndSub

========165

PrivateSub RebalanceRightGrew(parent As AVLNode)
Dimchild As AVLNode
Dimgrandchild As AVLNode

Setchild = parent.RightChild

Ifchild.Balance = RIGHT_HEAVY Then
'Выполнить левоевращение.
Setparent.RightChild = child.LeftChild
Setchild.LeftChild = parent
parent.Balance= BALANCED
Setparent = child
Else
'Выполнитьвращениевправо влево.
Setgrandchild = child.LeftChild
Setchild.LeftChild = grandchild.RightChild
Setgrandchild.RightChild = child
Setparent.RightChild = grandchild.LeftChild
Setgrandchild.LeftChild = parent
Ifgrandchild.Balance = RIGHT_HEAVY Then
parent.Balance= LEFT_HEAVY
Else
parent.Balance= BALANCED
EndIf
Ifgrandchild.Balance = LEFT_HEAVY Then
child.Balance= RIGHT_HEAVY
Else
child.Balance= BALANCED
EndIf
Setparent = grandchild
EndIf ' End if для правоговращения else двойноеправое
'вращение.
parent.Balance= BALANCED
EndSub
Удалениеузла из АВЛ дерева
В 6 главебыло показано, что удалитьэлемент изупорядоченногодерева сложнее, чем вставитьего. Если удаляемыйэлемент имеетвсего одногопотомка, можнозаменить егоэтим потомком, сохранив приэтом порядокдерева. Еслиу дерева двадочерних узла, то он заменяетсяна самый правыйузел в левойветви дерева.Если у этогоузла существуетлевый потомок, то этот левыйпотомок такжезанимает егоместо.

======166

Таккак АВЛ деревьяявляются особымтипом упорядоченныхдеревьев, тодля них нужновыполнить теже самые шаги.Тем не менее, после их завершениянеобходимовернуться назадпо дереву, чтобыубедиться втом, что оноосталосьсбалансированным.Если найдетсяузел, для которогоне выполняетсясвойствоАВЛ деревьев, то нужно выполнитьдля балансировкидерева соответствующеевращение. Хотяэто те же самыевращения, которыеиспользовалисьраньше длявставки узлав дерево, ониприменяютсяв других случаях.Левоевращение
Предположим, что мы удаляемузел из левогоподдерева узлаX. Также предположим, что правоеподдерево либоуравновешено, либо высотаего правойполовины наединицу больше, чем высоталевой. Тогдалевое вращение, показанноена рис. 7.12, приведетк балансировкедерева в узлеX.
Нижнийуровень поддереваT2 закрашенсерым цветом, чтобы показать, что поддеревоTB либо уравновешено(T2 и T3 имеютодинаковуювысоту), либоего праваяполовина выше(T3 выше, чемT2). Другимисловами, закрашенныйуровень можетсуществоватьв поддеревеT2 или отсутствовать.
ЕслиT2 и T3 имеютодинаковуювысоту, то высотаподдерева TXс корнем в узлеX не меняетсяпосле удаленияузла. ВысотаTX при этомостается равнойвысоте поддереваT2 плюс 2. Таккак эта высотане меняется, то дерево вышеэтого узлаостаетсясбалансированным.
ЕслиT3 выше, чем T2, то поддеревоTX становитсяниже на единицу.В этом случае, дерево можетбыть несбалансированнымвыше узла X, поэтомунеобходимопродолжитьпроверку дерева, чтобы определить, выполняетсяли свойствоАВЛ деревьевдля предковузла X.Вращениевправо влево
Предположимтеперь, чтоузел удаляетсяиз левого поддереваузла X, но леваяполовина правогоподдерева выше, чем правая.Тогда длябалансировкидерева нужноиспользоватьвращениевправо влево, показанноена рис. 7.13.
Еслилевое или правоеподдеревьяT2 или T3 выше, то вращениевправо влевоприведет кбалансировкеподдерева TX, и уменьшит приэтом высотуTX на единицу.Это значит, чтодерево вышеузла X можетбыть несбалансированным, поэтому необходимопродолжитьпроверку выполнениясвойства АВЛ деревьевдля предковузла X.

@Рис.7.12. Левое вращениепри удаленииузла

========167

@Рис.7.13. Вращениевправо влевопри удаленииузла
Другиевращения
Остальныевращения выполняютсяаналогично.В этом случаеудаляемый узелнаходится вправом поддеревеузла X. Эти четыревращения —те же самые, которые использовалисьдля балансировкидерева привставке узла, за одним исключением.
Еслиновый узелвставляетсяв дерево, топервое выполняемоевращение осуществляетбалансировкуподдерева TX, не изменяя еговысоту. Этозначит, чтодерево вышеузла TX будетпри этом оставатьсясбалансированным.Если же этивращения используютсяпосле удаленияузла из дерева, то вращениеможет уменьшитьвысоту поддереваTX на единицу.В этом случае, нельзя бытьуверенным, чтодерево вышеузла X осталосьсбалансированным.Нужно продолжитьпроверку выполнениясвойства АВЛ деревьеввверх по дереву.Реализацияудаления узловна языке VisualBasic
ПодпрограммаDeleteItemудаляет элементыиз дерева. Онарекурсивноспускаетсяпо дереву впоиске удаляемогоэлемента икогда она находитискомый узел, то удаляет его.Если у этогоузла нет потомков, то процедуразавершается.Если есть толькоодин потомок, то процедуразаменяет узелего потомком.
Еслиузел имеет двухпотомков, процедураDeleteItemвызывает процедуруReplaceRightMostдля заменыискомого узласамым правымузлом в еголевой ветви.ПроцедураReplaceRightMostвыполняетсяпримерно также, как и процедураиз 6 главы, котораяудаляет элементыиз обычного(неупорядоченного)дерева. Основноеотличие возникаетпри возвратеиз процедурыи рекурсивномпроходе вверхпо дереву. Приэтом процедураReplaceRightMostиспользуетвосходящуюрекурсию, чтобыубедиться, чтодерево остаетсясбалансированнымдля всех узлов.
Прикаждом возвратеиз процедуры, экземплярпроцедурыReplaceRightMostвызывает подпрограммуRebalanceRightShrunk, чтобы убедиться, что дерево вэтой точкесбалансировано.Так как процедураReplaceRightMostопускаетсяпо правой ветви, то она всегдаиспользуетдля выполнениябалансировкиподпрограммуRebalanceRightShrunk, а не RebalanceLeftShrunk.
Припервом вызовеподпрограммыReplaceRightMostпроцедураDeleteItemнаправляетее по левой отудаляемогоузла ветви. Привозврате изпервого вызоваподпрограммыReplaceRightMost, процедураDeleteItemиспользуетподпрограммуRebalanceLeftShrunk, чтобы убедиться, что деревосбалансированов этой точке.

=========168

Послеэтого, один задругим происходятрекурсивныевозвраты изпроцедурыDeleteItemпри проходедерева в обратномнаправлении.Так же, как ипроцедураReplaceRightmost, процедураDeleteItemвызывает подпрограммыRebalanceRightShrunkили RebalanceLeftShrunkв зависимостиот того, по какомупути происходитспуск по дереву.
ПодпрограммаRebalanceLeftShrunkаналогичнаподпрограммеRebalanceRightShrunk, поэтому онане показанав следующемкоде.

PublicSub DeleteItem(node As AVLNode, txt As String, shrunk As Boolean)
Dimchild As AVLNode
Dimtarget As AVLNode

Ifnode Is Nothing Then
Beep
MsgBox«Элемент » &txt & " не содержитсяв дереве."
shrunk= False
ExitSub
EndIf

Iftxt
Setchild = node.LeftChild
DeleteItemchild, txt, shrunk
Setnode.LeftChild = child
Ifshrunk Then RebalanceLeftShrunk node, shrunk
ElseIftxt > node.Box.Caption Then
Setchild = node.RightChild
DeleteItemchild, txt, shrunk
Setnode.RightChild = child
Ifshrunk Then RebalanceRightShrunk node, shrunk
Else
Settarget = node
Iftarget.RightChild Is Nothing Then
'Потомков нетили есть толькоправый.
Setnode = target.LeftChild
shrunk= True
ElseIftarget.LeftChild Is Nothing Then
'Есть толькоправый потомок.
Setnode = target.RightChild
shrunk= True
Else
'Есть два потомка.
Setchild = target.LeftChild
ReplaceRightmostchild, shrunk, target
Settarget.LeftChild = child
Ifshrunk Then RebalanceLeftShrunk node, shrunk
EndIf
EndIf
EndSub

PrivateSub ReplaceRightmost(repl As AVLNode, shrunk As Boolean, target AsAVLNode)
Dimchild As AVLNode

Ifrepl.RightChild Is Nothing Then
target.Box.Caption= repl.Box.Caption
Settarget = repl
Setrepl = repl.LeftChild
shrunk= True
Else
Setchild = repl.RightChild
ReplaceRightmostchild, shrunk, target
Setrepl.RightChild = child
Ifshrunk Then RebalanceRightShrunk repl, shrunk
EndIf
EndSub

PrivateSub RebalanceRightShrunk(node As AVLNode, shrunk As Boolean)
Dimchild As AVLNode
Dimchild_bal As Integer
Dimgrandchild As AVLNode
Dimgrandchild_bal As Integer

Ifnode.Balance = RIGHT_HEAVY Then
'Правая частьперевешивала, теперь балансвосстановлен.
node.Balance= BALANCED
ElseIfnode.Balance = BALANCED Then
'Было сбалансировано, теперь перевешиваетлевая часть.
node.Balance= LEFT_HEAVY
shrunk= False
Else
'Левая частьперевешивала, теперь несбалансировано.
Setchild = node.LeftChild
child_bal= child.Balance
Ifchild_bal
'Правое вращение.
Setnode.LeftChild = child.RightChild
Setchild.RightChild = node
Ifchild_bal = BALANCED Then
node.Balance= LEFT_HEAVY
child.Balance= RIGHT_HEAVY
shrunk= False
Else
node.Balance= BALANCED
child.Balance= BALANCED
EndIf
Setnode = child
Else
'Вращениевлево вправо.
Setgrandchild = child.RightChild
grandchild_bal= grandchild.Balance
Setchild.RightChild = grandchild.LeftChild
Setgrandchild.LeftChild = child
Setnode.LeftChild = grandchild.RightChild
Setgrandchild.RightChild = node
Ifgrandchild_bal = LEFT_HEAVY Then
node.Balance= RIGHT_HEAVY
Else
node.Balance= BALANCED
EndIf
Ifgrandchild_bal = RIGHT_HEAVY Then
child.Balance= LEFT_HEAVY
Else
child.Balance= BALANCED
EndIf
Setnode = grandchild
grandchild.Balance= BALANCED
EndIf
EndIf
EndSub

ПрограммаAVLоперируетАВЛ деревом.Введите тексти нажмите накнопку Add, чтобы добавитьэлемент к дереву.Введите значение, и нажмите накнопку Remove, чтобы удалитьэтот элементиз дерева. Нарис. 7.14 показанапрограмма AVL.Б деревья
Б деревья(B trees)являются другойформой сбалансированныхдеревьев, немногоболее наглядной, чем АВЛ деревья.Каждый узелв Б дереве можетсодержатьнесколькоключей данныхи несколькоуказателейна дочерниеузлы. Посколькукаждый узелсодержит несколькоэлементов, такие узлыиногда называютсяблоками.

=======171

@Рис.7.14. ПрограммаAVL

Междукаждой паройсоседних указателейнаходится ключ, который можноиспользоватьдля определенияветви, по которойнужно следоватьпри вставкеили поискеэлемента. Например, в дереве, показанномна рис. 7.15, корневойузел содержитдва ключа: G иR. Чтобы найтиэлемент созначением, которое идетперед G, нужноискать в первойветви. Чтобынайти элемент, имеющий значениемежду G и R, проверяетсявторая ветвь.Чтобы найтиэлемент, которыйследует за R, выбираетсятретья ветвь.
Б деревопорядка K обладаетследующимисвойствами:
Каждый узел содержит не более 2 * K ключей.
Каждый узел, кроме может быть корневого, содержит не менее K ключей.
Внутренний узел, имеющий M ключей, имеет M + 1 дочерних узлов.
Все листья дерева находятся на одном уровне.
Б деревона рис. 7.15 имеет2 порядок. Каждыйузел можетиметь до 4 ключей.Каждый узел, кроме можетбыть корневого, должен иметьне менее двухключей. Дляудобства, узлыБ дерева обычноимеют четноечисло ключей, поэтому порядокдерева обычноявляется целымчислом.
Выполнениетребования, чтобы каждыйузел Б­деревапорядка K содержалот K до 2 * K ключей, поддерживаетдерево сбалансированным.Так как каждыйузел должениметь не менееK ключей, он долженпри этом иметьне менее K + 1дочерних узлов, поэтому деревоне может статьслишком высокими тонким. Наибольшаявысота Б дерева, содержащегоN узлов, можетбыть равнаO(logK+1(N)). Это означает, что сложностьалгоритмапоиска в такомдереве порядкаO(log(N)). Хотя этои не так очевидно, операции вставкии удаленияэлемента изБ дерева такжеимеют сложностьпорядка O(log(N)).

@Рис.7.15. Б дерево

=======172
ПроизводительностьБ деревьев
ПрименениеБ деревьевособенно полезнопри разработкебольших приложений, работающихс базами данных.При достаточнобольшом порядкеБ дерева, любойэлемент в деревеможно найтипосле проверкивсего несколькихузлов. Например, высота Б дерева10 порядка, содержащегомиллион записей, не может бытьбольше log11(1.000.000), или выше шестиуровней. Чтобынайти определенныйэлемент, потребуетсяпроверить неболее шестиузлов.
Сбалансированноедвоичное деревос миллиономэлементов имелобы высотуlog2(1.000.000), или около20. Тем не менее, узлы двоичногодерева содержатвсего по одномуключевомузначению. Дляпоиска элементав двоичномдереве, пришлосьбы проверить20 узлов и 20 значений.Для поискаэлемента вБ дереве пришлосьбы проверить5 узлов и 100 ключей.
ПрименениеБ деревьевможет обеспечитьболее высокуюскорость работы, если проверкаключей выполняетсяотносительнопросто, в отличиеот проверкиузлов. Например, если база данныхнаходится надиске, чтениеданных с дискаможет происходитьдостаточномедленно. Когдаже данные находятсяв памяти, ихпроверка можетпроисходитьочень быстро.
Чтениеданных с дискапроисходитбольшими блоками, и считываниецелого блоказанимает столькоже времени, сколько и чтениеодного байта.Если узлы Б дереване слишкомвелики, то чтениеузла Б деревас диска займетне больше времени, чем чтение узладвоичногодерева. В этомслучае, дляпоиска 5 узловв Б деревепотребуетсявыполнить 5медленныхобращений кдиску, плюс 100быстрых обращенийк памяти. Поиск20 узлов в двоичномдереве потребует20 медленныхобращений кдиску и 20 быстрыхобращений кпамяти, приэтом поиск вдвоичном деревебудет болеемедленным, посколькувремя, затраченноена 15 лишнихобращений кдиску будетнамного больше, чем сэкономленноевремя 80 обращенийк памяти. Вопросы, связанные собращениемк диску, позднееобсуждаютсяв этой главеболее подробно.Вставкаэлементов вБ дерево
Чтобывставить новыйэлемент в Б дерево, найдем лист, в который ондолжен бытьпомещен. Еслиэтот узел содержитменее, чем 2 * Kключей, то вэтом узле остаетсяместо для добавлениянового элемента.Вставим новыйузел на местотак, чтобы порядокэлементоввнутри узлане нарушился.
Еслиузел уже содержит2 * K элементов, то места длянового элементав узле уже неостается. Разобьемтогда узел надва новых узла, поместив вкаждый из нихK элементов вправильномпорядке. Затемсредний элементпереместимв родительскийузел.
Например, предположим, что мы хотимпоместить новыйэлемент Q в Б дерево, показанноена рис. 7.15. Этотновый элементдолжен находитьсяво втором листе, который ужезаполнен. Дляразбиения этогоузла, разделимэлементы J, K, L, N иQ между двумяновыми узлами.Поместим элементыJ и K в левый узел, а элементы N иQ — в правый.Затем переместимсредний элемент,Lв родительскийузел. На рис.7.16 показано новоедерево.
    продолжение
--PAGE_BREAK--
=========xiv

В главе11 обсуждаютсяметоды сохраненияи поиска элементов, работающиедаже быстрее, чем это возможнопри использованиидеревьев, сортировкиили поиска. Вэтой главетакже описанынекоторыеметоды хэширования, включая использованиеблоков и связныхсписков, и нескольковариантовоткрытой адресации.
В главе12 описана другаякатегорияалгоритмов —сетевые алгоритмы.Некоторые изэтих алгоритмов, такие как вычислениекратчайшегопути, непосредственноприменимы кфизическимсетям. Эти алгоритмытакже могуткосвенно применятьсядля решениядругих задач, которые напервый взглядне кажутсясвязаннымис сетями. Например, алгоритмыпоиска кратчайшегорасстояниямогут разбиватьсеть на районыили определятькритичныезадачи в расписаниипроекта.
В главе13 объясняютсяметоды, применениекоторых сталовозможнымблагодарявведению классовв 4 й версииVisual Basic. Этиметоды используютобъектно ориентированныйподход дляреализациинетипичногодля традиционныхалгоритмовповедения.

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

Аппаратныетребования
Дляработы с примерамивам потребуетсякомпьютер, конфигурациякоторогоудовлетворяеттребованиямдля работыпрограммнойсреды VisualBasic. Эти требованиявыполняютсяпочти для всехкомпьютеров, на которыхможет работатьоперационнаясистема Windows.
Накомпьютерахразной конфигурацииалгоритмывыполняютсяс различнойскоростью.Компьютер спроцессоромPentium Pro с тактовойчастотой 2000 МГци 64 Мбайт оперативнойпамяти будетработать намногобыстрее, чеммашина с 386 процессороми всего 4 Мбайтпамяти. Вы быстроузнаете, на чтоспособно вашеаппаратноеобеспечение.
Измененияво втором издании
Самоебольшое изменениев новой версииVisual Basic —это появлениеклассов. Классыпозволяютрассмотретьнекоторыезадачи с другойстороны, позволяяиспользоватьболее простойи естественныйподход к пониманиюи применениюмногих алгоритмов.Изменения вкоде программв этом изложении используютпреимущества, предоставляемыеклассами. Ихможно разбитьна три категории:
Замена псевдоуказателей классами. Хотя все алгоритмы, которые были написаны для старых версий VB, все еще работают, многие из тех, что были написаны с применением псевдоуказателей (описанных во 2 главе), гораздо проще понять, используя классы.
Инкапсуляция. Классы позволяют заключить алгоритм в компактный модуль, который легко использовать в программе. Например, при помощи классов можно создать несколько связных списков и не писать при этом дополнительный код для управления каждым списком по отдельности.
Объектно ориентированные технологии. Использование классов также позволяет легче понять некоторые объектно ориентированные алгоритмы. В главе 13 описываются методы, которые сложно реализовать без использования классов.
Какпользоватьсяэтим материалом
В главе1 даются общиепонятия, которыеиспользуютсяна протяжениивсего изложения, поэтому вамследует начатьчтение с этойглавы. Вам стоитознакомитьсяс этой тематикой, даже если выне хотите сразуже достичьглубокогопониманияалгоритмов.
В 6 главеобсуждаютсяпонятия, которыеиспользуютсяв 7, 8 и 12 главах, поэтому вамследует прочитать6 главу до того, как братьсяза них. Остальныеглавы можночитать в любомпорядке.

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

В табл.1 показаны тривозможныхучебных плана, которыми выможете руководствоватьсяпри изученииматериала взависимостиот того, насколькошироко вы хотитеознакомитьсяс алгоритмами.Первый планвключает в себяосвоение основныхметодов и структурданных, которыемогут бытьполезны приразработкевами собственныхпрограмм. Второйкроме этогоописывает такжеосновные алгоритмы, такие как алгоритмысортировкии поиска, которыемогут понадобитьсяпри написанииболее сложныхпрограмм.
Последнийплан дает порядокдля изучениявсего материалацеликом. Хотя7 и 8 главы логическивытекают из6 главы, они сложнеедля изучения, чем следующиеглавы, поэтомуони изучаютсянесколькопозже.
Почемуименно VisualBasic?
Наиболеечасто встречаютсяжалобы на медленноевыполнениепрограмм, написанныхна Visual Basic.Многие другиекомпиляторы, такие как Delphi,Visual C++ даютболее быстрыйи гибкий код, и предоставляютпрограммистуболее мощныесредства, чемVisual Basic.Поэтому логичнозадать вопрос —«Почему я должениспользоватьименно VisualBasic для написаниясложных алгоритмов? Не лучше былобы использоватьDelphi или C++ или, по крайнеймере, написатьалгоритмы наодном из этихязыков и подключатьих к программамна Visual Basicпри помощибиблиотек?»Написаниеалгоритмовна Visual Basicимеет смыслпо несколькимпричинам.
Во первых, разработкаприложенияна Visual C++ гораздосложнее ипроблематичнее, чем на VisualBasic. Некорректнаяреализацияв программевсех деталейпрограммированияпод Windows можетпривести ксбоям в вашемприложении, среде разработки, или в самойоперационнойсистеме Windows.
Во вторых, разработкабиблиотекина языке C++ дляиспользованияв программахна Visual Basicвключает в себямного потенциальныхопасностей, характерныхи для приложенийWindows, написанныхна C++. Если библиотекабудет неправильновзаимодействоватьс программойна Visual Basic, она также приведетк сбоям в программе, а возможно ив среде разработкии системе.
В-третьих, многие алгоритмыдостаточноэффективныи показываютнеплохуюпроизводительностьдаже при применениине очень быстрыхкомпиляторов, таких, как VisualBasic. Например, алгоритм сортировкиподсчетом,

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

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

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

=============xviii
Глава1. Основные понятия
В этойглаве содержатсяобщие понятия, которые нужноусвоить передначалом серьезногоизучения алгоритмов.Начинаетсяона с вопроса«Что такоеалгоритмы?».Прежде чемуглубитьсяв детали программированияалгоритмов, стоит потратитьнемного времени, чтобы разобратьсяв том, что этотакое.
Затемв этой главедается введениев формальнуютеорию сложностиалгоритмов(complexity theory).При помощи этойтеории можнооценить теоретическуювычислительнуюсложностьалгоритмов.Этот подходпозволяетсравниватьразличныеалгоритмы ипредсказыватьих производительностьв разных условиях.В главе приводитсянесколькопримеров применениятеории сложностик небольшимзадачам.
Некоторыеалгоритмы свысокой теоретическойпроизводительностьюне слишкомхорошо работаютна практике, поэтому в даннойглаве такжеобсуждаютсянекоторыереальные предпосылкидля созданияпрограмм. Слишкомчастое обращениек файлу подкачкиили плохоеиспользованиессылок на объектыи коллекцииможет значительноснизить производительностьпрекрасногов остальныхотношенияхприложения.
Послезнакомствас основнымипонятиями, высможете применятьих к алгоритмам, изложеннымв последующихглавах, а такжедля анализасобственныхпрограмм дляоценки ихпроизводительностии сможетепредугадыватьвозможныепроблемы дотого, как ониобернутсякатастрофой.Что такоеалгоритмы?
Алгоритм –это последовательностьинструкцийдля выполнениякакого либозадания. Когдавы даете кому тоинструкциио том, как отремонтироватьгазонокосилку, испечь торт, вы тем самымзадаете алгоритмдействий. Конечно, подобные бытовыеалгоритмыописываютсянеформально, например, так:

Проверьте, находится лимашина на стоянке.
Убедитесь, что машинапоставленана ручной тормоз.
Повернитеключ.
Ит.д.

==========1

Приэтом по умолчаниюпредполагается, что человек, который будетследовать этиминструкциям, сможет самостоятельновыполнитьмножествомелких операций, например, отперетьи открыть дверь, сесть за руль, пристегнутьремень, найтиручной тормози так далее.
Еслиже составляетсяалгоритм дляисполнениякомпьютером, вы не можетеполагатьсяна то, что компьютерпоймет что либо, если это неописано заранее.Словарь компьютера(язык программирования)очень ограничени все инструкциидля компьютерадолжны бытьсформулированына этом языке.Поэтому длянаписаниякомпьютерныхалгоритмовиспользуетсяформализованныйстиль.
Интереснопопробоватьнаписатьформализованныйалгоритм дляобычных ежедневныхзадач. Например, алгоритм вождениямашины мог бывыглядетьпримерно так:

Еслидверь закрыта:
Вставитьключ в замок
Повернутьключ
Еслидверь остаетсязакрытой, то:
Повернутьключ в другуюсторону
Повернутьручку двери
Ит.д.

Этотфрагмент «кода»отвечает толькоза открываниедвери; при этомдаже не проверяется, какая дверьоткрывается.Если дверьзаело или вмашине установленапротивоугоннаясистема, тоалгоритм открываниядвери можетбыть достаточносложным.
Формализациейалгоритмовзанимаютсяуже тысячи лет.За 300 лет до н.э.Евклид написалалгоритмыделения угловпополам, проверкиравенстватреугольникови решения другихгеометрическихзадач. Он началс небольшогословаря аксиом, таких как«параллельныелинии не пересекаются»и построил наих основе алгоритмыдля решениясложных задач.
Формализованныеалгоритмытакого типахорошо подходятдля задач математики, где должна бытьдоказана истинностькакого либоположения иливозможностьвыполнениякаких то действий, скорость жеисполненияалгоритма неважна. Для выполненияреальных задач, связанных свыполнениеминструкций, например задачисортировкина компьютерезаписей о миллионепокупателей, эффективностьвыполнениястановитсяважной частьюпостановкизадачи.Анализскорости выполненияалгоритмов
Естьнесколькоспособов оценкисложностиалгоритмов.Программистыобычно сосредотачиваютвнимание наскорости алгоритма, но важны и другиетребования, например, кразмеру памяти, свободномуместу на дискеили другимресурсам. Отбыстрого алгоритмаможет быть малотолку, если поднего требуетсябольше памяти, чем установленона компьютере.Пространство —время
Многиеалгоритмыпредоставляютвыбор междускоростьювыполненияи используемымипрограммойресурсами.Задача можетвыполняться
быстрее, используябольше памяти, или наоборот, медленнее, заняв меньшийобъем памяти.

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

Хорошимпримером вданном случаеможет служитьалгоритм нахождениякратчайшегопути. Задавкарту улицгорода в видесети, можнонаписать алгоритм, вычисляющийкратчайшеерасстояниемежду любымидвумя точкамив этой сети.Вместо тогочтобы каждыйраз зановопересчитыватькратчайшеерасстояниемежду двумязаданнымиточками, можнозаранее просчитатьего для всехпар точек исохранитьрезультатыв таблице. Тогда, чтобы найтикратчайшеерасстояниедля двух заданныхточек, достаточнобудет простовзять готовоезначение изтаблицы.
Приэтом мы получимрезультатпрактическимгновенно, ноэто потребуетбольшого объемапамяти. Картаулиц для большогогорода, такогокак Бостон илиДенвер, можетсодержать сотнитысяч точек.Для такой сетитаблица кратчайшихрасстоянийсодержала быболее 10 миллиардовзаписей. В этомслучае выбормежду временемисполненияи объемом требуемойпамяти очевиден: поставивдополнительные10 гигабайтоперативнойпамяти, можнозаставитьпрограммувыполнятьсягораздо быстрее.
Из этойсвязи вытекаетидея пространственно временнойсложностиалгоритмов.При этом подходесложностьалгоритмаоцениваетсяв терминахвремени ипространства, и находитсякомпромиссмежду ними.
В этомматериалеосновное вниманиеуделяетсявременнойсложности, номы также постаралисьобратить вниманиеи на особыетребованияк объему памятидля некоторыхалгоритмов.Например, сортировкаслиянием (mergesort), обсуждаемаяв 9 главе, требуетбольше временнойпамяти. Другиеалгоритмы, напримерпирамидальнаясортировка(heapsort), котораятакже обсуждаетсяв 9 главе, требуетобычного объемапамяти.Оценкас точностьюдо порядка
Присравненииразличныхалгоритмовважно понимать, как сложностьалгоритмасоотноситсясо сложностьюрешаемой задачи.При расчетахпо одному алгоритмусортировкатысячи чиселможет занять1 секунду, асортировкамиллиона —10 секунд, в товремя как расчетыпо другомуалгоритму могутпотребовать2 и 5 секундсоответственно.В этом случаенельзя однозначносказать, какаяиз двух программлучше — этобудет зависетьот исходныхданных.
Различиепроизводительностиалгоритмовна задачахразной вычислительнойсложности частоболее важно, чем простоскорость алгоритма.В вышеприведенномслучае, первыйалгоритм быстреесортируеткороткие списки, а второй —длинные.
Производительностьалгоритма можнооценить попорядку величины.Алгоритм имеетсложностьпорядка O(f(N))(произносится«О большое отF от N»), если времявыполненияалгоритмарастет пропорциональнофункции f(N)с увеличениемразмерностиисходных данныхN. Например, рассмотримфрагмент кода, сортирующийположительныечисла:

ForI = 1 To N
'Поискнаибольшегоэлемента всписке.
MaxValue= 0
ForJ = 1 to N
IfValue(J) > MaxValue Then
MaxValue= Value(J)
MaxJ= J
EndIf
NextJ
'Выводнаибольшегоэлемента напечать.
PrintFormat$(MaxJ) & ":" & Str$(MaxValue)
'Обнулениеэлемента дляисключенияего из дальнейшегопоиска.
Value(MaxJ)= 0
NextI

===============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). Приведемиллюстрирующийэтот случайфрагмент кода:
    продолжение
--PAGE_BREAK--
@Рис.7.16. Б дерево послевставки элементаQ

=========173

Разбиениеузла на дваназываетсяразбиениемблока. Когдаоно происходит, к родительскомуузлу добавляетсяновый ключ иновый указатель.Если родительскийузел уже заполнен, то это такжеможет привестик его разбиению.Это, в свою очередь, потребуетдобавленияновой записина более высокомуровне и такдалее. В наихудшемслучае, вставкаэлемента вызовет«цепную реакцию», которая приведетк изменениямна всех вышележащихуровнях вплотьдо разбиениякорневого узла.
Когдапроисходитразбиениекорневого узла, Б дерево становитсявыше. Это единственныйслучай, прикотором еговысота увеличивается.Поэтому Б деревьяобладают необычнымсвойством —они всегдарастут от листьевк корню.Удалениеэлементов изБ дерева
Теоретически, удалить узелиз Б дереватак же просто, как и вставитьего. На практике, детали этогопроцесса достаточносложны.
Еслиудаляемый узелне являетсялистом, то егонужно заменитьдругим элементом, чтобы сохранитьпорядок элементов.Это похоже наслучай удаленийэлемента изупорядоченногодерева илиАВЛ дереваи его можнообрабатыватьаналогично.Заменим элементсамым крайнимправым элементомиз левой ветви.Этот элементвсегда будетлистом. Послезамены элемента, можно простосчитать, чтовместо негопросто удалензаменившийего лист.
Чтобыудалить элементиз листа, вначаленужно принеобходимостисдвинуть вседругие элементывлево, чтобызаполнитьобразовавшеесяпространство.Помните, чтокаждый узелв Б деревепорядка K должениметь от K до2 * K элементов.После удаленияэлемента излиста, можетоказаться, чтоон содержитвсего K - 1 элементов.
В этомслучае, можнопопробоватьвзять несколькоэлементов изузлов на томже уровне. Затемможно распределитьэлементы в двухузлах так, чтобыони оба имелине меньше Kэлементов. Нарис. 7.17 элементудаляется изсамого левоголиста дерева, при этом в немостается всегоодин элемент.После перераспределенияэлементов междуузлом и правымузлом на томже уровне, обаузла имеют неменьше двухключей. Заметьте, что среднийэлемент J перемещаетсяв родительскийузел.

@Рис.7.17. Балансировкапосле удаленияэлемента

=======174

@Рис.7.18. Слияние послеудаления элемента

Припопытке сбалансироватьдерево такимобразом, можетоказаться, чтососедний узелна том же уровнесодержит всегоK элементов.Тогда два узлавместе содержатвсего 2 * K - 1элементов, чтонедостаточнодля заполнениядвух узлов. Вэтом случае, все элементыиз обоих узловмогут поместитьсяв одном узле, поэтому ихможно слить.Удалим ключ, который отделяетдва узла отродителя. Поместимэтот элементи 2 * K - 1 элементовиз двух узловв один общийузел. Этот процессназываетсяслиянием узлов(bucket merge илиbucket join). Нарис. 7.18 показанослияние двухузлов.
Прислиянии двухузлов, из родительскогоузла удаляетсяключ, при этомв родительскомузле можетостаться K - 1элементов. Вэтом случае, может потребоватьсябалансировкаили слияниеродителя содним из узловна его уровне.Это также можетпривести ктому, что в узлена более высокомуровне такжеостанется K - 1элементов, ипроцесс повторится.В наихудшемслучае, удалениеприведет к«цепной реакции»слияний блоков, которая можетдойти до корневогоузла.
Приудалении последнегоэлемента изкорневого узла, два его оставшихсядочерних узласливаются, образуя новыйкорень, и деревопри этом становитсякороче на одинуровень. Единственныйспособ уменьшениявысоты Б дерева —слияние двухдочерних узловкорня и образованиенового корня.
ПрограммаBtreeпозволяет вамоперироватьБ деревом.Введите текст, и нажмите накнопку Add, чтобы добавитьэлемент в дерево.Для удаленияэлемента введитеего значениеи нажмите накнопку Remove.На рис. 7.19 показаноокно программыBtreeс Б деревом2 порядка.

@Рис.7.19. ПрограммаBtree

========175
РазновидностиБ деревьев
СуществуетнесколькоразновидностейБ деревьев, из которыхздесь описанытолько некоторые.НисходящиеБ деревья(top downB trees)немного иначеуправляютструктуройБ дерева. Засчет разбиениявстречающихсяполных узлов, эта разновидностьалгоритмаиспользуетпри вставкеэлементов болеенагляднуюнисходящуюрекурсию вместовосходящей.Эта также уменьшаетвероятностьвозникновениядлительнойпоследовательностиразбиенийблоков.
ДругойразновидностьюБ деревьевявляются Б+деревья(B+trees). В Б+деревьяхвнутренниеузлы содержаттолько ключиданных, а самизаписи находятсяв листьях. ЭтопозволяетБ+деревьямхранить в каждомблоке большеэлементов, поэтому такиедеревья короче, чем соответствующиеБ деревья.НисходящиеБ деревья
Подпрограмма, которая добавляетновый элементв Б дерево, вначале выполняетрекурсивныйпоиск по дереву, чтобы найтиблок, в которыйего нужно поместить.Когда она пытаетсявставить новыйэлемент на егоместо, ей можетпонадобитьсяразбить блоки переместитьодин из элементовузла в егородительскийузел.
Привозврате изрекурсивныхвызовов процедуры, вызывающаяпроцедурапроверяет, требуется лиразбиениеродительскогоузла. Если да, то элементпомещаетсяв родительскийузел. При каждомвозврате изрекурсивноговызова, вызывающаяпроцедурадолжна проверять, не требуетсяли разбиениеследующегопредка. Так какэти разбиенияблоков происходятпри возвратеиз рекурсивныхвызовов процедура, это восходящаярекурсия, поэтомуиногда Б деревья, которыми манипулируюттаким образом, называютсявосходящимиБ деревьями(bottom upB trees).
Другаястратегиясостоит в том, чтобы разбиватьвсе полныеузлы, которыевстречаютсяпроцедуре напути вниз подереву. Припоиске блока, в который нужнопоместить новыйэлемент, процедураразбивает всеповстречавшиесяполные узлы.При каждомразбиении узла, она помещаетодин из егоэлементов вродительскийузел. Так какона уже разбилавсе выше расположенныеполные узлы, то в родительскомузле всегдаесть место длянового элемента.
Когдапроцедурадоходит долиста, в которыйнужно поместитьэлемент, то вего родительскомузле всегдаесть свободноеместо, и еслипрограмме нужноразбить лист, то всегда можнопоместитьсредний элементв родительскийузел. Так какпри этом процедураработает сдеревом сверхувниз, Б деревьятакого типаиногда называютсянисходящимиБ деревьями(top downB trees).
Приэтом разбиениеблоков происходитчаще, чем этоабсолютнонеобходимо.В нисходящемБ дереве полныйузел разбивается, даже если в егодочерних узлахдостаточномного свободногоместа. За счетпредварительногоразбиенияузлов, прииспользованиинисходящегометода в деревесодержитсябольше пустогопространства, чем в восходящемБ дереве. Сдругой стороны, такой подходуменьшаетвероятностьвозникновениядлительнойпоследовательностиразбиенийблоков.
К сожалению, не существуетнисходящейверсии дляслияния узлов.При продвижениивниз по дереву, процедураудаления узловне может объединятьвстречающиесянаполовинупустые узлы, потому что вэтот моментеще неизвестно, нужно ли будетобъединитьдва дочернихузла и удалитьэлемент из ихродителя. Таккак неизвестнотакже, будетли удален элементиз родительскогоузла, то нельзязаранее сказать, потребуетсяли слияниеродителя содним из узлов, находящимсяна том же уровне.

==========176
Б+деревья
Б+деревьячасто используютсядля хранениябольших записей.Типичное Б деревоможет содержатьзаписи о сотрудниках, каждая из которыхможет заниматьнесколькокилобайт памяти.Записи моглибы располагатьсяв Б дереве всоответствиис ключевымполем, напримерфамилией сотрудникаили его идентификационнымномером.
В этомслучае упорядочениеэлементов можетбыть достаточномедленным.Чтобы слитьдва блока, программеможет понадобитьсяпереместитьмножествозаписей, каждаяиз которыхможет бытьдостаточнобольшой. Аналогично, для разбиенияблока можетпотребоватьсяпереместитьмножествозаписей большогообъема.
Чтобыизбежать перемещениябольших блоковданных, программаможет записыватьво внутреннихузлах Б дереватолько ключи.При этом узлытакже содержатссылки на самизаписи данных, которые записаныв другом месте.Теперь, еслипрограмметребуетсяпереупорядочитьблоки, то нужнопереместитьтолько ключии указатели, а не сами записи.Этот тип Б дереваназываетсяБ+деревом(B+tree).
То, чтоэлементы вБ+дереве достаточномалы, такжепозволяетпрограммехранить большеключей в каждомузле. При томже размереузла, программаможет увеличитьпорядок дереваи сделать егоболее коротким.
Например, предположим, что имеетсяБ дерево 2 порядка, то есть каждыйузел имеет оттрех до пятидочерних узлов.Такое дерево, содержащеемиллион записей, должно былобы иметь высотумежду log5(1.000.000) иlog3(1.000.000), или между9 и 13. Чтобы найтиэлемент в такомдереве, программадолжна выполнитьот 9 до 13 обращенийк диску.
Теперьдопустим, чтоте же миллионзаписей находятсяв Б+дереве, узлыкоторого имеютпримерно тотже размер вбайтах. Посколькув узлах Б+деревасодержатсятолько ключи, то в каждомузле дереваможет хранитьсядо 20 ключей кзаписям. В этомслучае, каждыйузел будетиметь от 11 до21 дочерних узлов, поэтому высотадерева будетот log21(1.000.000) доlog11(1.000.000), или между5 и 6. Чтобы найтиэлемент, программепонадобитсявсего 6 обращенийк диску длянахожденияего ключа, иеще одно обращениек диску, чтобысчитать самэлемент.
В Б+деревьяхтакже простосвязать с наборомзаписей множествоключей. В системе, оперирующейзаписями осотрудниках, одно Б+деревоможет использоватьв качествеключей фамилии, а другое —идентификационныеномера социальногострахования.Оба деревабудут содержатьуказатели назаписи данных, которые будутнаходитьсяза пределамидеревьев.УлучшениепроизводительностиБ деревьев
В этомразделе описаныдва методаулучшенияпроизводительностиБ  и Б+деревьев.Первый методпозволяетперераспределитьэлементы междуузлами одногоуровня, чтобыизбежать разбиенияблоков. Второйпозволяетпомещать пустыеячейки в дерево, чтобы уменьшитьвероятностьнеобходимостиразбиенияблоков в будущем.

=======177
Балансировкадля устраненияразбиенияблоков
Придобавленииэлемента кблоку, которыйуже заполнен, блок разбиваетсяна два. Этогоможно избежать, если выполнитьбалансировкуэтого узла содним из узловна том же уровне.Например, вставканового элементаQ в Б дерево, показанноеслева на рис.7.20 обычно вызываетразбиениеблока. Этогоможно избежать, выполнив балансировкуузла, содержащегоJ, K, L и N и левогоузла на том жеуровне, содержащегоB и E. При этомполучаетсядерево, показанноена рис. 7.20 справа.
Такаябалансировкаимеет рядпреимуществ.Во первых, приэтом блокииспользуютсяболее эффективно.В них находитсяменьше пустыхячеек, при этомуменьшитсяколичестворасходуемойпонапраснупамяти.
Чтоболее важно, если не нужнобудет разбиениеблоков, то непонадобитсяи перемещениеэлемента вродительскийузел. Это предотвращаетвозникновениедлительнойпоследовательностиразбиенийблоков.
С другойстороны, уменьшениечисла неиспользуемыхэлементов вдереве увеличиваетвероятностьнеобходимостиразбиенияблоков в будущем.Так как в деревеостается меньшесвободныхячеек, то болеевероятно, чтоузел окажетсяуже полон, когдапонадобитсявставить новыйэлемент.Добавлениесвободногопространства
Предположим, что имеетсянебольшая базаданных клиентов, содержащая10 записей. Можнозагружатьзаписи в Б деревотак, чтобы онизаполняликаждый блокцеликом, какпоказано нарис. 7.21. При этомдерево содержитмало свободногопространства, и вставка новогоэлемента сразуже приводитк разбиениюблоков. Фактически, так как всеблоки заполнены, она вызоветпоследовательностьразбиенияблоков, котораядойдет до корневогоузла.
Вместоплотного заполнениядерева, можнодобавлять ккаждому узлунекотороеколичествопустых ячеек, как показанона рис. 7.22. Хотяпри этом деревобудет несколькобольше, в негоможно будетдобавлятьэлементы, невызывая сразуже последовательностьразбиенийблоков. Послеработы с деревомв течение некотороговремени, количествосвободногопространстваможет уменьшитьсядо такой степени, при которойразбиенияблоков могутвозникнуть.Тогда можноперестроитьдерево, добавивбольше свободногопространства.
В реальныхприложенияхБ деревья обычноимеют намногобольший порядок, чем деревья, приведенныездесь. Добавлениесвободногопространствав дерево значительноуменьшаетнеобходимостьбалансировкии разбиенияблоков. Например, можно добавитьв Б дерево 10порядка 10 процентовсвободногопространства, чтобы в каждомузле было местоеще для двухэлементов. Стаким деревомможно будетработать достаточнодолго, преждечем возникнутдлинные цепочкиразбиенийблоков.
Этоочереднойпример пространственно временногокомпромисса.Добавка в узлыпустого пространстваувеличиваетразмер дерева, но уменьшаетвероятностьразбиенияблоков.

@Рис.7.20. Балансировкадля устраненияразбиенияблоков

=======178

@Рис.7.21. Плотное заполнениеБ дерева
Вопросы, связанные собращениемк диску
Б  иБ+деревья хорошоподходят длясоздания большихприложенийбаз данных.Типичное Б+деревоможет содержатьсотни, тысячии даже миллионызаписей. В этомслучае в любоймомент временив памяти будетнаходитьсятолько небольшаячасть дереваи при каждомобращении кузлу, программепонадобитсязагрузить егос диска. В этомразделе описанытри момента, учитыватькоторые особенноважно, еслиданные находятсяна диске: применениепсевдоуказателей, выбор размераблоков, и кэшированиекорневого узла.Псевдоуказатели
Коллекциии ссылки наобъекты удобныдля построениядеревьев впамяти, но онимогут бытьбесполезныпри хранениидерева на диске.Нельзя создатьссылку на записьв файле.
Вместоэтого можноиспользоватьметоды работыс псевдоуказателями, похожие на те, которые былиописаны во 2главе. Вместоиспользованияв качествеуказателейна узлы деревассылок на объектыпри этом используетсяномер записиузла в файле.Предположим, что Б+дерево12 порядка использует80 байтные ключи.Структуруданных узламожно определитьв следующемкоде:

GlobalConst ORDER = 12
GlobalConst KEYS_PER_NODE = 2 * ORDER

TypeBtreeNode
Key(1 To KEYS_PER_NODE) As String * 80 ' Ключи.
Child(0 To KEYS_PER_NODE) As Integer ' Указателипотомков.
EndType

Значенияэлементовмассива Childпредставляютсобой номеразаписей издочерних узловв файле. Произвольныйдоступ к даннымБ+дерева изфайла осуществляетсяпри помощизаписей, которыесоответствуютструктуреBtreeNode.

@Рис.7.22. СвободноезаполнениеБ дерева

======179

Dimnode As BtreeNode

OpenFilename For Random As #filenum Len = Len(node)

Послеоткрытия файла, при помощиоператора Getможно выбратьлюбую запись:

Dimnode As BtreeNode

'Выбрать записьс номером recnum.
Get#filenum, recnum, node

Чтобыупроститьработу с Б+деревьями, можно хранитьузлы Б+дереваи записи данныхв разных файлахи использоватьдля управлениякаждым из нихпсевдоуказатели.
Когдасчетчик ссылокна объект становитсяравным нулю, то Visual Basicавтоматическиуничтожаетего. Это облегчаетработу со структурамиданных в памяти.С другой стороны, если программебольше не нужнакакая либозапись в файле, то она не можетпросто очиститьвсе ссылки нанее. Если сделатьтак, то программабольше не сможетиспользоватьэту запись, нозапись по прежнемубудет заниматьместо в файле.
Программадолжна следитьза неиспользуемымизаписями, чтобыпозднее можнобыло использоватьих. Один из простыхспособов сделатьэто — вестисвязный списокнеиспользуемыхзаписей. Еслизапись большене нужна, онадобавляетсяв список. Еслипрограмме нужноместо для новойзаписи, онаудаляет однузапись из списка.Если программенужно вставитьеще один элемент, а список пуст, она увеличиваетфайл данных.Выборразмера блока
Чтениеданных с дискапроисходитблоками, которыеназываютсякластерами.Размер кластераобычно составляет512 или 1024 байта, или еще какое либочисло байтов, равное степенидвойки. Чтениевсего кластеразанимает столькоже времени, сколько и чтениеодного байта.
Можновоспользоватьсяэтим фактоми создаватьблоки, размеркоторых составляетцелое числокластеров, азатем уместитьв этот размермаксимальноечисло ключейили записей.Например, предположим, что мы решилисоздавать блокиразмером 2048 байт.При созданииБ+дерева с80 байтнымиключами в каждыйблок можнопоместить 24ключа и 25 указателей(если указательпредставляетсобой 4 байтноечисло типаlong).Затем можносоздать Б+дерево12 порядка с блоками, которые определяютсяв следующемкоде:

GlobalConst ORDER = 12
GlobalConst KEYS_PER_NODE = 2 * ORDER
TypeBtreeNode
Key(1To KEYS_PER_NODE) As String * 80 ' Ключданных.
Child(0To KEYS_PER_NODE) As Integer ' Указателипотомков.
EndType
    продолжение
--PAGE_BREAK--
=======180

Длятого, чтобысчитыватьданные максимальнобыстро, программадолжна использоватьоператор VisualBasic Getдля чтения узлацеликом. Еслииспользоватьцикл Forдля чтенияключей и данныхдля каждогоэлемента поочереди, топрограммепридется обращатьсяк диску причтении каждогоэлемента. Этонамного медленнее, чем считываниевсего узласразу. В одномиз тестов, длямассива из 1000элементовопределенногопользователемтипа чтениеэлементов поодиночке занялов 27 раз большевремени, чемчтение их всехсразу. Следующийкод демонстрируетоба способачтения данныхиз узла:

Dimi As Integer
Dimnode As BtreeNode

'Медленныйспособ доступак данным.
Fori = 1 To KEYS_PER_NODE
Get#filenum,, node.Key(i)
Nexti

'Быстрый способдоступа к данным.
Get#filenum,, node
Кэшированиеузлов
Каждыйпоиск в Б деревеначинаетсяс корневогоузла. Можноускорить поиск, если корневойузел будет всевремя находитьсяв памяти. Тогдаво время поискапридется наодин раз меньшеобращатьсяк диску. Приэтом все равнонеобходимозаписыватькорневой узелна диск прикаждом егоизменении, иначе при повторнойзагрузке послеотказа программыизменения вБ дереве будутпотеряны.
Можнотакже кэшироватьв памяти и другиеузлы Б дерева.Если хранитьв памяти вседочерние узлыкорня, то ихтакже не потребуетсясчитывать сдиска. Для Б деревапорядка K, корневойузел будетиметь от 1 до2 * K ключей ипоэтому у негобудет от 2 до2 * K + 1 дочернихузлов. Это значит, что в этом случаепридется кэшироватьдо 2 * K + 1 узлов.
Программатакже можеткэшироватьузлы при обходеБ дерева. Например, при прямомобходе программаобращаетсяк каждому узлуи затем рекурсивнообходит всеего дочерниеузлы. При этомона вначалеспускаетсяк первому дочернемуузлу, а послевозврата переходитк следующему.При каждомвозврате, программадолжна сноваобратитьсяк родительскомуузлу, чтобыопределить, к какому издочерних узловобращатьсяв следующуюочередь. Кэшируяродительскийузел в памяти, программаизбегаетнеобходимостиснова считыватьего с диска.
Применениерекурсии позволяетпрограммеавтоматическисохранять узлыв памяти безиспользованиясложной схемыкэширования.При каждомвызове рекурсивногоалгоритмаобхода, определяетсялокальнаяпеременная, в которой находитсяузел до техпор, пока он непонадобится.При возвратеиз рекурсивноговызова VisualBasic автоматическиосвобождаетэту переменную.Следующий коддемонстрирует, как можно реализоватьэтот алгоритмобхода на языкеVisual Basic.

=======181

PrivateSub PreorderPrint(node_index As Integer)
Dimi As Integer
Dimnode As BtreeNode

Get#filenum, node_index, node ' Кэшироватьузел.
Printnode_index ' Обращениек узлу.
Fori = 0 To KEYS_PER_NODE
Ifnode.Child(i)
PreorderPrintnode.Child(i) ' Вызовпотомка.
Nexti
EndSub
База данныхна основе Б+дерева
ПрограммаBplusработает сбазой данныхна основе Б+дерева, используя двафайла данных.Файл Custs.DATсодержит записис данными оклиентах, афайл Custs.IDX —узлы Б+дерева.
Чтобыдобавить новуюзапись в базуданных, введитеданные в полеCustomer Record(Запись о клиенте), и затем нажмитена кнопку Add.Для поисказаписи заполнитеполя LastName (Фамилия)и First Name(Имя) в верхнейчасти формыи нажмите накнопку Find(Найти).
На рис.7.23 показано окнопрограммы послевыполненияпоиска записидля Рода Стивенса.Статистикавнизу показывает, что данные былинайдены в записиномер 302 послевсего лишь трехобращений кдиску. ВысотаБ+дерева в программеравна 3, и оносодержит 1303 записейданных и 118 блоков.
Когдавы вводитезапись илипроводитепоиск, программаBplusвыбирает этузапись из файла.После нажатияна кнопку Removeпрограммаудаляет записьиз базы данных.

@Рис.7.23. ПрограммаBplus

========182

Есливыбрать в менюDisplay (Показать)команду InternalNodes (Внутренниеузлы), то программавыведет списоквнутреннихузлов дерева.Она также выводитрядом с каждымузлом ключи, чтобы показатьвнутреннююструктурудерева.
Припомощи командыComplete Tree(Все дерево) изменю Displayможно вывестиструктурудерева целиком.Данные о клиентахвыводятсявнутри пунктирныхскобок.
Кромеобычных полейадреса и фамилии, программа Bplusтакже включаетполе NextGarbage, которое программаиспользуетдля работы сосвязным спискомнеиспользуемыхв файле записей.

TypeCustRecord
LastNameAs String * 20
FirstNameAs String * 20
AddressAs String * 40
CityAs String * 20
StateAs String * 2
ZipAs String * 10
PhoneAs String * 12
NextGarbageAs Long
EndType

'Размер записиданных о клиенте.
GlobalConst CUST_SIZE = 20 + 20 + 40 + 20 + 2 +10 + 12 + 4

Внутренниеузлы Б+деревасодержат ключи, которые используютсядля поискаданных о клиенте.Ключом длязаписи являетсяфамилия клиента, дополненнаяв конце пробеламидо 20 символови заканчивающаясязапятой, закоторой следуетимя клиента, дополненноепробелами до20 символов.Например,«Washington..........,George..............».При этом полнаядлина ключасоставляет41 символ.
Каждыйвнутреннийузел такжесодержит указателина дочерниеузлы. Эти указателиопределяютположениезаписей с даннымио клиенте вфайле Custs.DAT.Узлы такжевключают переменнуюNumKeys, которая содержитчисло используемыхключей.
Программачитает и пишетданные блокамипримерно по1024 байта. Еслипредположить, что блок содержитK ключей, то вкаждом блокебудет K ключейдлиной 41 байт,K + 1 указателейна дочерниеузлы длинойпо 4 байта, идвухбайтноецелое числоNumKeys.При этом блокидолжны иметьмаксимальновозможныйразмер и бытьне больше 1024 байт.
Решивуравнение41 * K + 4 * (K + 1) + 2

=======183

ConstKEY_SIZE = 41
ConstORDER = 11
GlobalConst KEYS_PER_NODE = 2 * ORDER

TypeBucket
NumKeysAs Integer
Key(1To KEYS_PER_NODE) As String * KEY_SIZE
Child(0To KEYS_PER_NODE) As Long
EndType
GlobalConst BUCKET_SIZE = 2 + _
KEYS_PER_NODE* KEY_SIZE + _
(KEYS_PER_NODE+ 1) * 4

ПрограммаBplusзаписываетблоки Б+деревав файле Custs.IDX.Первая записьв этом файлесодержит заголовок, который описываеттекущее состояниеБ+дерева. В заголовоквходит указательна корневойузел, текущаявысота дерева, указатель напервый пустойблок в файлеCusts.IDX, и указательна первый пустойблок в файлеCusts.DAT.
Чтобыупроститьчтение и записьзаголовка, можно определитьеще одну структуру, которая имеетв точноститакой же размер, что и блокиданных, но содержитполя заголовка.Последнее полев определении —это строка, которая заполняетконец структуры, чтобы ее размербыл точно равенразмеру блока.

GlobalConst HEADER_PADDING = _
BUCKET_SIZE- (7 * 4 + 2)
TypeHeaderRecord
NumBucketsAs Long
NumRecordsAs Long
RootAs Long
NextTreeRecordAs Long
NextCustRecordAs Long
FirstTreeGarbageAs Long
FirstCustGarbageAs Long
HeightAs Integer
PaddingAs String * HEADER_PADDING
EndType

Призапуске программыона запрашиваетдиректорию, в которой находятсяданные, и затемоткрывает файлыCusts.DATфайлы Custs.IDXв этой директории.Если эти файлыне существуют, то программаих создает. Впротивномслучае, онасчитываетзаголовок синформациейо дереве изфайла Custs.IDX.Затем она считываеткорневой узелБ+дерева и кэшируетего в памяти.
Спускаясьпо дереву привставке илиудалении элемента, программакэширует элементы, к которым онаобращается.При рекурсивномвозврате этиузлы могутпонадобитьсяснова, еслипроисходилоразбиение, слияние илидругое переупорядочениеузлов. Так какпрограммакэширует узлына пути сверхувниз, они будутдоступны привозвращенииобратно.
Увеличениеразмера блоковпозволяетсделать Б+деревьяболее эффективными, но при этомтестироватьих вручнуюбудет сложнее.Чтобы высотаБ+дерева 11 порядкастала равна2, необходимодобавить к базеданных 23 элемента.Чтобы увеличитьвысоту деревадо 3 уровня, необходимодобавить более250 дополнительныхэлементов.

=======184

Чтобыбыло прощетестироватьпрограммуBplus, вы можете захотетьуменьшитьпорядок Б+деревадо 2. Для этогозакомментируйтев файле Bplus.BASстроку, котораяопределяет11 порядок, и уберитекомментарийиз строки, котораязадает 2 порядок:

'ConstORDER = 11
ConstORDER = 2

КомандаCreate Data(Создать данные)в меню Data(Данные) позволяетбыстро создатьмножествозаписей данных.Введите числозаписей, которыевы хотите создать, и число, котороепрограммадолжна использоватьдля созданияпервого элемента.Затем программасоздаст записии вставит ихв Б+дерево. Например, если задатьв программесоздание 100 записей, начиная созначения 200, топрограммасоздаст записи200, 201, … 299, которыебудут выглядетьтак:

FirstName: First0000200
LastName: Last0000200
Address: Addr0000200
Cuty: City0000200
Резюме
Применениесбалансированныхдеревьев впрограммепозволяетэффективноработать сданными. Длязаписи большихбаз данных надисках илидругих относительномедленныхзапоминающихустройствахособенно удобныБ+деревья высокогопорядка. Болеетого, можноиспользоватьнесколькоБ+деревьев длясоздания несколькихиндексов одногои того же большогонабора данных.
В главе11 описана альтернативасбалансированнымдеревьям. Хешированиев некоторыхслучаях позволяетдобиться ещеболее быстрогодоступа к данным, хотя оно и непозволяетвыполнять такиеоперации, какпоследовательныйвывод записей.

========185
Глава8. Деревья решений
Многиесложные реальныезадачи можносмоделироватьпри помощидеревьев решений(decision trees).Каждый узелдерева представляетодин шаг решениязадачи. Каждаяветвь в деревепредставляетрешение, котороеведет к болееполному решению.Листья представляютсобой окончательноерешение. Цельзаключаетсяв том, чтобынайти «наилучший»путь от корняк листу привыполненииопределенныхусловий. Этиусловия и значениепонятия «наилучший»для пути зависитот задачи.
Деревьярешений обычноимеют громадныйразмер. Дереворешений дляигры в крестики ноликисодержит болееполумиллионаузлов. Эта иградовольно проста, и многие реальныезадачи намногоболее сложны.Соответствующиеим деревьярешений моглибы содержатьбольше узлов, чем число атомовво вселенной.
В этойглаве обсуждаютсяметоды, которыеможно использоватьдля поиска втаких огромныхдеревьях. Во первых, в ней вначалерассматриваютсядеревья игры(game trees). Напримере игрыв крестики ноликиобсуждаютсяспособы поискав деревьях игрыдля нахождениянаилучшеговозможногохода.
В следующихразделах описываютсяспособы поискав более общихдеревьях решений.Для самых маленькихдеревьев, можноиспользоватьметод полногоперебора(exhaustive searching)всех возможныхрешений. Длядеревьев большегоразмера, можноиспользоватьметод ветвейи границ(branch and boundtechnique) позволяетнайти наилучшеерешение безнеобходимостивыполнять поискпо всему дереву.
Дляочень большихдеревьев нужноиспользоватьэвристическийметод илиэвристику(heuristic). При этомполученноерешение можетбыть не наилучшимиз возможныхрешений, нооно, тем не менее, лежит достаточноблизко к наилучшему, чтобы его можнобыло использовать.Используяэвристики, можно проводитьпоиск практическив любых деревьяхрешений.
В концеэтой главыобсуждаютсянекоторые оченьсложные задачи, которые выможете попытатьсярешить припомощи методаветвей и границили эвристическогометода. Многиеиз этих задачимеют важныеприменения, и нахождениехороших решенийдля них крайненеобходимо.Поискв деревьях игры
Стратегиюнастольныхигр, таких какшахматы, шашки, или крестики ноликиможно смоделироватьпри помощидеревьев игры.Если в какойто момент игрысуществует30 возможныхходов, то соответствующийузел в деревеигры будетиметь 30 ветвей.

========187

Например, для игры вкрестики ноликикорневой узелсоответствуетначальнойпозиции, прикоторой доскапуста. Первыйигрок можетпоместитькрестик в любуюиз девяти клетокдоски. Каждомуиз этих девятивозможных ходовсоответствуетвыходящая изкорня ветвь.Девять узловна конце этиветвей соответствуютдевяти различнымпозициям послепервого ходаигрока.
Послетого, как первыйигрок сделалход, второйможет поставитьнолик в любуюиз оставшихсявосьми клеток.Каждому из этихходов соответствуетветвь, выходящаяиз узла, соответствующеготекущей позицииигры. На рис.8.1 показан небольшойфрагмент дереваигры в крестики нолики.
Какможно увидетьна рис. 8.1, деревоигры в крестики ноликирастет оченьбыстро. Еслионо продолжитрасти такимобразом, такчто каждыйследующий узелв дереве будетиметь на однуветвь меньше, чем его родитель, то дерево целикомбудет иметь9 * 8 * 7 … * 1 = 362.880 листьев.В дереве будет362.880 возможныхпутей, соответствующих362.800 возможнымиграм.
В действительностимногие из узловдерева будутотсутствовать, так как соответствующиеим ходы запрещеныправилами игры.Если игрок, ходивший первым, за три своиххода поставиткрестики вверхней левой, верхней среднейи верхней правойклетках, то онвыиграет и игразакончится.Узел, соответствующийэтой позиции, не будет иметьпотомков, таккак игра завершаетсяна этом шаге.Эта игра показанана рис. 8.2.
Послеудаления всехневозможныхузлов в деревеостается околочетверти миллионалистьев. Этовсе еще оченьбольшое дерево, и поиск егометодом полногоперебора занимаетдостаточномного времени.Для более сложныхигр, таких какшашки, шахматыили го, деревьяигры имеютогромный размер.Если бы во времякаждого ходав шахматахигрок имел 16возможныхвариантов, тодерево игрыимело бы болеетриллиона узловпосле пятиходов каждогоиз игроков. Вконце этойглавы обсуждаетсяпоиск в такихогромных деревьяхигры, а следующийраздел посвященболее простомупримеру игрыв крестики нолики.

@Рис.8.1. Фрагмент дереваигры в крестики нолики

========188

@Рис.8.2. Быстрое окончаниеигры
Минимаксныйпоиск
Длявыполненияпоиска в деревеигры, нужноиметь возможностьопределитьвес позициина доске. Дляигры в крестики нолики, для первогоигрока большийвес имеют позиции, в которых трикрестика расположеныв ряд, так какпри этом первыйигрок выигрывает.Вес тех же позицийдля второгоигрока мал, потому, что вэтом случаеон проигрывает.
Длякаждого игрока, можно присвоитьпозиции одиниз четырехвесов. Если весравен 4, то этозначит, чтоигрок в этойпозиции выигрывает.Если вес равен3, то из текущегоположения надоске неясно, кто из игроковвыиграет вконце концов.Вес, равный 2, означает, чтопозиция приводитк ничьей. И, наконец, вес, равный 1, означает, чтовыигрываетпротивник.
Дляпоиска дереваметодом полногоперебора можноиспользоватьминимаксную(minimax) стратегию, при которойделается попыткаминимизироватьмаксимальныйвес, которыйможет иметьпозиция дляпротивникапосле следующегохода. Это можносделать, определивмаксимальновозможный веспозиции дляпротивникапосле каждогоиз своих возможныхходов, и затемвыбрав ход, который даетпозицию с минимальнымвесом для противника.
ПодпрограммаBoardValue, приведеннаяниже, вычисляетвес позициина доске, проверяявсе возможныеходы. Для каждогохода она рекурсивновызывает себя, чтобы найтивес, которыйбудет иметьновая позициядля противника.Затем она выбираетход, при которомвес полученнойпозиции дляпротивникабудет наименьшим.
Дляопределениявеса позициина доске процедураBoardValueрекурсивновызывает себядо тех пор, покане произойдетодно из трехсобытий. Во первых, она может дойтидо позиции, вкоторой игроквыигрывает.В этом случае, она присваиваетпозиции вес4, что указываетна выигрышигрока, совершившегопоследний ход.

======189

Во вторых, процедураBoardValueможет найтипозицию, в которойни один из игроковне может совершитьследующий ход.Игра при этомзаканчиваетсяничьей, поэтомупроцедураприсваиваетэтой позициивес 2.
И наконец, процедура можетдостигнутьзаданной максимальнойглубины рекурсии.В этом случае, процедураBoardValueприсваиваетпозиции вес3, что указывает, что она не можетопределитьпобедителя.Задание максимальнойглубины рекурсииограничиваетвремя поискав дереве игры.Это особенноважно для болеесложных игр, таких как шахматы, в которых поискв дереве игрыможет продолжатьсяпрактическивечно. Максимальнаяглубина поискатакже можетзадавать уровеньмастерствапрограммы. Чемдальше впередпрограммасможет анализироватьходы, тем лучшеона будет играть.
На рис.8.3 показано деревоигры в крестики ноликив конце партии.Ходит игрок, играющий крестиками, и у него естьтри возможныххода. Чтобывыбрать наилучшийход, процедураBoardValueрекурсивнопроверяеткаждый из трехвозможныхходов. Первыйи третий возможныеходы (левая иправая ветвидерева) приводятк выигрышупротивника, поэтому их весдля противникаравен 4. Второйвозможный ходприводит кничьей, и еговес для противникаравен 2. ПроцедураBoardValueвыбирает этотход, так как онимеет наименьшийвес для противника.
    продолжение
--PAGE_BREAK--
@Рис.8.3. Нижняя частьдерева игры

PrivateSub BoardValue(best_move As Integer, best_value As Integer, pl1 AsInteger, pl2 As Integer, Depth As Integer)
Dimpl As Integer
Dimi As Integer
Dimgood_i As Integer
Dimgood_value As Integer
Dimenemy_i As Integer
Dimenemy_value As Integer

DoEvents 'Не занимать100% процессорноговремени.

'Если глубинарекурсии слишкомвелика, результатнеизвестен.
IfDepth >= SkillLevel Then
best_value= VALUE_UNKNOWN
ExitSub
EndIf

'Если игразавершается, то результатизвестен.
pl= Winner()
Ifpl PLAYER_NONE Then
'Преобразоватьвес для победителяpl в вес для игрокаpl1.
Ifpl = pl1 Then
best_value= VALUE_WIN
ElseIfpl = pl2 Then
best_value= VALUE_LOSE
Else
best_value= VALUE_DRAW
EndIf
ExitSub
EndIf

'Проверить вседопустимыеходы.
good_i= -1
good_value= VALUE_HIGH
Fori = 1 To NUM_SQUARES
'Проверить ход, если он разрешенправилами.
IfBoard(i) = PLAYER_NONE Then
'Найти вес полученногоположения дляпротивника.
IfShowTrials Then _
MoveLabel.Caption= _
MoveLabel.Caption& Format$(i)
'Сделать ход.
Board(i)= pl1
BoardValueenemy_i, enemy_value, pl2, pl1, Depth + 1
'Отменить ход.
Board(i)= PLAYER_NONE
IfShowTrials Then _
MoveLabel.Caption= _
Left$(MoveLabel.Caption,Depth)

'Меньше ли этотвес, чем предыдущий.
Ifenemy_value
good_i= i
good_value= enemy_value
'Если мы выигрываем, то лучшегорешения нет,
'поэтому выбираетсяэтот ход.
Ifgood_value
EndIf
EndIf ' End if Board(i) = PLAYER_NONE ...
Nexti

'Преобразоватьвес позициидля противникав вес для игрока.
Ifgood_value = VALUE_WIN Then
'Противниквыигрывает, мы проиграли.
best_value= VALUE_LOSE
ElseIfenemy_value = VALUE_LOSE Then
'Противникпроиграл, мывыиграли.
best_value= VALUE_WIN
Else
'Вес ничьей илинеопределеннойпозиции
'одинаков дляобоих игроков.
best_value= good_value
EndIf
best_move= good_i
EndSub

ПрограммаTicTacиспользуетпроцедуруBoardValue.Основная частькода программыобеспечиваетвзаимодействиес пользователем, рисует доску, позволяетпользователювыбрать ход, задавать опциии так далее.
Еслине выбранакоманда ShowTest Moves(Показыватьпроверяемыеходы) из менюOptions (Опции), то производительностьпрограммы будетнамного выше.Если выбранаэта опция, топрограммавыводит каждыйанализируемыйход. Постоянноеобновлениеэкрана занимаетнамного большевремени, чемдействительныйпоиск в дереве.
Другиекоманды в менюOptions позволяютвам, выбратьуровень мастерствапрограммы(максимальнуюглубину рекурсии)и выбрать игрукрестикамиили ноликами.При высокомуровне мастерствапервый ходзанимает намногобольше времени.

=====192
Сдача
ПодпрограммаBoardValueимеет интересныйпобочный эффект.Если она находитдва одинаковохороших хода, то она выбираетиз них первыйпопавшийся.Иногда этоприводит кстранномуповедениюпрограммы.Например, еслипрограммаопределяет, что при любомсвоем ходе онапроигрывает, то она выбираетпервый из них.Иногда этотход может показатьсячеловеку глупым.Может создатьсявпечатление, что компьютервыбрал случайныйход и сдается.В какой то степениэто действительнотак.
Например, запустим программуTicTacс третьим уровнеммастерства.Перенумеруемклетки так, какпоказано нарис. 8.4. Сделаемпервых ход вклетку 6. Программавыберет клетку1. Выберем клетку3, программаответит ходомна клетку 9. Теперь, если занятьклетку 5, тонаступаетвыигрыш, еслиследующим ходомпойти на клетку4 или 7.
Компьютертеперь можетпросмотретьдерево игрыдо конца и убедитьсяв своем проигрыше.В такой ситуациичеловек попыталсябы заблокироватьодин из выигрышныхходов, либопоместить дванолика в ряд, чтобы попытатьсявыиграть наследующем ходу.В более сложнойигре, такой какшахматы, человектакже можетвыбрать однуиз этих стратегий, в надежде нато, что соперникне увидит путик победе. Соперникможет ошибиться, давая игрокутем самым шансна победу.
Программаже считает, чтопротивникиграет безошибочнои также знаето своем выигрыше.Так как ни одинход не приводитк победе, топрограммавыбирает первыйпопавшийсяход, в данномслучае занимаетклетку 2. Этотход кажетсяглупым, так какон не блокируетни одного извозможныхвыигрышныхходов, и не делаетпопытку выигратьна следующемходу. При этомкажется, чтокомпьютерсдается. Этаигра показанана рис. 8.5.
Одиниз способовпредотвращениятакого поведениясостоит в том, чтобы задатьбольше различныхвесов позиций.В программеTicTacвсе проигрышныепозиции имеютодинаковыйвес. Можно присвоитьпозиции, в которойпроигрыш происходитза два хода, больший вес, чем позиции, в которой проигрышнаступает наследующем ходу.Тогда программасможет выбиратьходы, которыеприведут кзатягиваниюигры. Такжеможно присваиватьбольший веспозиции, в которойимеются двавозможныхвыигрышныххода, чем позиции, в которой естьтолько одинвыигрышныйход. В такомслучае компьютерпопытался бызаблокироватьодин из возможныхвыигрышныхходов.Улучшениепоиска в деревеигры
Еслибы для поискав дереве игрымы располагалитолько минимакснойстратегией, то выполнитьпоиск в большихдеревьях былобы очень сложно.Такие игры, какшахматы, настолькосложны, чтопрограмма можетпровести поисквсего лишь нанесколькихуровнях дерева.К счастью, существуютнесколькоприемов, которыеможно использоватьдля поиска вбольших деревьяхигры.

@Рис.8.4. Нумерацияклеток доскиигры в крестики нолики

======193

@Рис.8.5. Программаигры в крестики ноликисдается
Предварительноевычислениеначальных ходов
Во первых, в программемогут бытьзаписаны начальныеходы, выбранныеэкспертами.Можно решить, что программаигры в крестики ноликидолжна делатьпервый ход вцентральнуюклетку. Этоопределяетпервую ветвьдерева игры, поэтому программаможет игнорироватьвсе пути, непроходящиечерез первуюветвь. Это уменьшаетдерево игрыв крестики ноликив 9 раз.
Фактически, программе ненужно выполнятьпоиск в дереведо того, покапротивник несделает свойход. В этот моменти компьютери противниквыбрали каждыйсвою ветвь, поэтому оставшеесядерево станетнамного меньше, и будет содержатьменее чем 7! = 5040путей. Просчитавзаранее всегоодин ход, можноуменьшитьразмер дереваигры от четвертимиллиона доменее чем 5040 путей.
Аналогично, можно записатьответы на первыеходы, если противникходит первым.Есть девятьвариантовпервого хода, следовательно, нужно записатьдевять ответныхходов. При этомпрограмме ненужно поводитьпоиск по дереву, пока противникне сделает двахода, а компьютер —один. Тогдадерево игрыбудет содержатьменее чем 6! = 720путей. Записановсего девятьходов, а размердерева при этомуменьшаетсяочень сильно.Это еще одинпример пространственно временногокомпромисса.Использованиебольшего количествапамяти уменьшаетвремя, необходимоедля поиска вдереве игры.
ПрограммаTicTac2использует10 записанныхходов. Задайте9 уровень мастерства, и пусть программаделает первыйход. Затем задайтете же опции впрограммеTicTac.Вы увидитегромаднуюразницу в скоростиработы этихдвух программ.
Коммерческиепрограммы игрыв шахматы такженачинают сзаписанныхходов и ответов, рекомендованныхгроссмейстерами.Такие программымогут делатьпервые ходыочень быстро.После того, какпрограммаисчерпает всезаписанныезаранее ходы, она начнетделать ходынамного медленнее.Определениеважных позиций
Другойспособ улучшенияпоиска в деревеигры состоитв том, чтобыопределятьважные позиции.Если программараспознаетодну из этихпозиций, онаможет выполнитьопределенныедействия илиизменить способпоиска в деревеигры.

========194

Во времяигры в шахматыигроки часторасполагаютфигура так, чтобы они защищалидругие фигуры.Если противникберет фигуру, то игрок беретфигуру противникавзамен. Частотакое взятиепозволяетпротивникув свою очередьвзять другуюфигуру, чтоприводит ксерии обменов.
Некоторыепрограммынаходят возможныепоследовательностейобменов. Еслипрограммараспознаетвозможностьобмена, она навремя изменяетмаксимальнуюглубину, накоторую онапросматриваетдерево, чтобыпроследитьдо конца цепочкуобменов. Этопозволяетпрограммерешить, стоитли идти на обмен.После обменафигур их количествотакже уменьшается, поэтому поискв дереве игрыстановитсяв будущем болеепростым.
Некоторыешахматныепрограммы такжеотслеживаютрокировки, ходы, при которыхпод боем оказываетсясразу несколькофигур, шах илинападение наферзя и такдалее.Эвристики
В играх, более сложных, чем крестики нолики, практическиневозможнопровести поискдаже в небольшомфрагментедерева игры.В этих случаях, можно использоватьразличныеэвристики.Эвристикойназывает алгоритмили эмпирическоеправило, котороевероятно, ноне обязательнодаст хорошийрезультат.
Например, в шахматахобычной эвристикойявляется «усилениепреимущества».Если у противникаменьше сильныхфигур и одинаковоечисло остальных, то следует идтина размен прикаждой возможности.Например, есливы берете коняпротивника, теряя при этомсвоего, то такойобмен следуетвыполнить.Уменьшениечисла оставшихсяфигур делаетдерево решенийкороче и можетувеличитьотносительноепреимущество.Эта стратегияне гарантируетвыигрыша, ноповышает еговероятность.
Другаячасто используемаяэвристиказаключаетсяв присвоенииразных весовразличнымчастям доски.В шахматах весклеток в центредоски выше, таккак фигуры, находящиесяна этих позициях, могут атаковатьбольшую частьдоски. КогдапроцедураBoardValueвычисляет вестекущей позициина доске, онаможет присваиватьбольший весфигурам, которыезанимают клеткив центре доски.Поискв других деревьяхрешений
Некоторыеметоды поискав деревьях игрынеприменимык обобщеннымдеревьям решений.Многие их этихдеревьев невключают поочередныхходов игроков, поэтому минимаксныйметод и вычисленныезаранее ходыв данном случаебессмысленны.В следующихразделах описаныметоды, которыеможно использоватьдля поиска вэтих типахдеревьев решений.

=======195
Методветвей и границ
Методветвей и границ(branch andbound) являетсяодним из методовотсечения(pruning) ветвейв дереве решений, чтобы не былонеобходиморассматриватьвсе ветви дерева.Общий подходпри этом состоитв том, чтобыотслеживатьграницы ужеобнаруженныхи возможныхрешений. Еслив какой то точкенаилучшее изуже найденныхрешений лучше, чем наилучшеевозможноерешение в нижнихветвях, то можноигнорироватьвсе пути внизот узла.
Например, допустим, чтоимеет 100 миллионовдолларов, которыенужно вложитьв нескольковозможныхинвестиций.Каждое из вложенийимеет разнуюстоимость идает разнуюприбыль. Необходиморешить, каквложить деньгинаилучшимобразом, чтобысуммарнаяприбыль быламаксимальной.
Задачитакого типаназываютсязадачей формированияпортфеля(knapsack problem).Имеется несколькопозиций (инвестиций), которые должныпоместитьсяв портфельфиксированногоразмера (100 миллионовдолларов). Каждаяиз позицийимеет стоимость(деньги) и цену(тоже деньги).Необходимонайти наборпозиций, которыйпомещаетсяв портфель иимеет максимальновозможную цену.
Этузадачу можносмоделироватьпри помощидерева решений.Каждый узелдерева соответствуетопределеннойкомбинациипозиций в портфеле.Каждая ветвьсоответствуетпринятию решенияо том, чтобыудалить позициюиз портфеляили добавитьее в него. Леваяветвь первогоузла соответствуетпервому вложению.На рис. 8.6 показанодерево решенийдля четырехвозможныхинвестиций.
Дереворешений дляэтой задачипредставляетсобой полноедвоичное дерево, глубина которогоравна числуинвестиций.Каждый листсоответствуетполному наборуинвестиций.
Размерэтого дереваочень быстрорастет с увеличениемчисла инвестиций.Для 10 возможныхинвестиций, в дереве будетнаходиться210 = 1024 листа.Для 20 инвестиций, в дереве будетуже более миллионалистьев. Можнопровести полныйпоиск по такомудереву, но придальнейшемувеличениичисла возможныхинвестицийразмер деревастанет оченьбольшим.

@Рис.8.6. Дерево решенийдля инвестиций

=======196

Чтобыиспользоватьметод ветвейи границ, создадиммассив, которыйбудет содержатьпозиции изнаилучшегонайденногодо сих пор решения.При инициализациимассив долженбыть пуст. Можнотакже использоватьпеременнуюдля отслеживанияцены этогорешения. Вначалеэта переменнаяможет иметьнебольшоезначение, чтобыпервое же найденноереальное решениебыло лучшеисходного.
Припоиске в дереверешений, еслив какой то точкеанализируемоерешение неможет бытьлучше, чемсуществующее, то можно прекратитьдальнейшийпоиск по этомупути. Также, если в какой тоточке выбранныепозиции стоятболее 100 миллионов, то можно такжепрекратитьпоиск.
В качествеконкретногопримера, предположим, что имеютсяинвестиции, приведенныев табл. 8.1. На рис.8.6 показаносоответствующеедерево решений.Некоторые изэтих инвестиционныхпакетов нарушаютграничныеусловия задачи.Например, самыйлевый путьпривел бы квложению 178миллионовдолларов вовсе четыревозможныхинвестиции.
Предположим, что мы началипоиск в дереве, изображенномна рис. 8.6 и обнаружили, что можно потратить97 миллионовдолларов напозиции Aи B, получив23 миллиона прибыли.Это соответствуетчетвертомулисту слевана рис. 8.6.
Припродолжениипоиска в дереве, можно дойтидо второгослева узла Bна рис. 8.6. Этосоответствуетинвестиционномупакету, которыйвключает позициюA, не включаетпозицию B, и может включатьили не включатьпозиции Cи D. В этойточке пакетуже стоит 45миллионовдолларов засчет позицииA, и приносит10 миллионовприбыли.
Оставшиесяпозиции Cи D вместевзятые могутповысить прибыльеще на 12 миллионов.Текущее решениеприносит 10 миллионовприбыли, поэтомунаилучшеевозможноерешение нижеэтого узлапринесет небольше 11 миллионовприбыли. Этоменьше, чемдоход в 23 миллионадля уже найденногорешения, поэтомунет смыслапродолжатьпоиск вниз поэтому пути.
По мерепродвиженияпрограммы подереву ей ненужно постояннопроверять, будет ли частичноерешение, котороеона рассматривает, лучше, чем наилучшеенайденное досих пор решение.Если частичноерешение лучше, то лучше будети самый правыйузел внизу отэтого частичногорешения. Этотузел представляеттот же самыйнабор позиций, как и частичноерешение, таккак все остальныепозиции приэтом исключены.Это означает, что программенеобходимоискать лучшеерешение толькотогда, когдаона достигаетлиста.

@Таблица8.1. Возможныеинвестиции

======197

Фактически, любой лист, докоторого доходитпрограммавсегда являетсяболее хорошимрешением. Еслибы это было нетак, то ветвь, на которомнаходится этотлист, была быотсечена, когдапрограммарассматривалародительскийузел. В этойточке перемещениек листу уменьшитцену невыбранныхпозиций донуля. Если ценарешения небольше, чемнаилучшеенайденное досих пор решение, то проверканижней границыостановитпродвижениепрограммы клисту. Используяэтот факт, программаможет обновлятьнаилучшеерешение придостижениилиста.
Следующийкод используетпроверку верхнейи нижней границыдля реализацииалгоритмаветвей и границ:

'Полная нераспределеннаяприбыль.
Privateunassigned_profit As Integer

PublicNumItems As Integer
PublicMaxItem As Integer

GlobalConst OPTION_EXHAUSTIVE_SEARCH = 0
GlobalConst OPTION_BRANCH_AND_BOUND = 1

TypeItem
CostAs Integer
ProfitAs Integer
EndType

GlobalItems() As Item
GlobalNodesVisited As Long
GlobalToSpend As Integer
Globalbest_cost As Integer
Globalbest_profit As Integer

'Равно True дляпозиций в текущемнаилучшемрешении.
Publicbest_solution() As Boolean

'Решение, котороемы проверяем.
Privatetest_solution() As Boolean
Privatetest_cost As Integer
Privatetest_profit As Integer

'Инициализацияпеременныхи начало поиска.
PublicSub Search(search_type As Integer)
Dimi As Integer

'Задание размерамассивов решения.
ReDimbest_solution(0 To MaxItem)
ReDimtest_solution(0 To MaxItem)

'Инициализация- пустой списокинвестиций.
NodesVisited= 0
best_profit= 0
best_cost= 0
unassigned_profit= 0
Fori = 0 To MaxItem
unassigned_profit= unassigned_profit + Items(i).Profit
Nexti
test_profit= 0
test_cost= 0

'Начнем поискс первой позиции.
BranchAndBound0
EndSub

'Выполнить поискметодом ветвейи границ начинаяс этой позиции.
PublicSub BranchAndBound(item_num As Integer)
Dimi As Integer

NodesVisited= NodesVisited + 1

'Если это лист, то это лучшеерешение, чем
'то, которое мыимели раньше, иначе он былбы
'отсечен вовремя поискараньше.
Ifitem_num > MaxItem Then
Fori = 0 To MaxItem
best_solution(i)= test_solution(i)
best_profit= test_profit
best_cost= test_cost
Nexti
ExitSub
EndIf
    продолжение
--PAGE_BREAK--
'Иначе перейтипо ветви внизпо ветвям потомка.
'Вначале попытатьсядобавить этупозицию. Убедиться,
'что она не превышаетограничениепо цене.
Iftest_cost + Items(item_num).Cost
'Добавить позициюк тестовомурешению.
test_solution(item_num)= True
test_cost= test_cost + Items(item_num).Cost
test_profit= test_profit + Items(item_num).Profit
unassigned_profit= unassigned_profit — Items(item_num).Profit

'Рекурсивнаяпроверка возможногорезультата.
BranchAndBounditem_num + 1

'Удалить позициюиз тестовогорешения.
test_solution(item_num)= False
test_cost= test_cost — Items(item_num).Cost
test_profit= test_profit — Items(item_num).Profit
unassigned_profit= unassigned_profit + Items(item_num).Profit
EndIf

'Попытатьсяисключитьпозицию. Выяснить, принесут ли
'оставшиесяпозиции достаточныйдоход, чтобы
'путь вниз поэтой ветвипревысил нижнийпредел.
unassigned_profit= unassigned_profit — Items(item_num).Profit
Iftest_profit + unassigned_profit > best_profit Then BranchAndBounditem_num + 1
unassigned_profit= unassigned_profit + Items(item_num).Profit
EndSub

ПрограммаBandBиспользуетметод полногоперебора иметод ветвейи границ длярешения задачио формированиипортфеля. Введитемаксимальнуюи минимальнуюстоимость ицену, которыевы хотите присвоитьпозициям, атакже числопозиций, котороетребуетсясоздать. Затемнажмите накнопку Randomize(Рандомизировать), чтобы создатьсписок позиций.
Затемпри помощипереключателявнизу формывыберите либоExhaustive Search(Полный перебор), либо Branchand Bound(Метод ветвейи границ). Когдавы нажмете накнопку Go(Начать), топрограмманайдет наилучшеерешение припомощи выбранногометода. Затемона выведетна экран эторешение, а такжечисло узловв полном дереверешений и числоузлов, которыепрограмма вдействительностипроверила. Нарис. 8.7 показаноокно программыBindBпосле решениязадачи портфелядля 20 позиций.Перед тем, каквыполнитьполный перебордля 20 позиций, попробуйтевначале запуститьпримеры меньшегоразмера. Накомпьютерес процессоромPentium с тактовойчастотой 90 МГцпоиск решениязадачи портфелядля 20 позицийметодом полногоперебора занялболее 30 секунд.
Припоиске методомветвей и границчисло проверяемыхузлов намногоменьше, чем приполном переборе.Дерево решенийдля задачипортфеля с 20позициямисодержит 2.097.151узел. При полномпереборе придетсяпроверить ихвсе, при поискеметодом ветвейи границ понадобитсяпроверитьтолько примерно1.500 из них.

@Рис.8.7. ПрограммаBindB

======200

Числоузлов, которыепроверяетпрограмма прииспользованииметода ветвейи границ, зависитот точных значенийданных. Еслицена позицийвысока, то вправильноерешение будетвходить немногоэлементов.После помещениянесколькихпозиций в пробноерешение, оставшиесяпозиции слишкомдорого стоят, чтобы поместитьсяв портфеле, потому большаячасть деревабудет отсечена.
С другойстороны, еслиэлементы имеютнизкую стоимость, то в правильноерешение войдетбольшое ихчисло, поэтомупрограммепридется исследоватьмножествокомбинаций.В табл. 8.2 приведеночисло узлов, проверенноепрограммойBindBв серии тестовпри различнойстоимостипозиций. Программасоздавала 20случайныхпозиций, и полнаястоимостьрешения быларавна 100.Эвристики
Иногдадаже алгоритмветвей и границне может провестиполный поискв дереве. Дереворешений длязадачи портфеляс 65 позициямисодержит более7 * 1019 узлов.Если алгоритмветвей и границпроверяеттолько однудесятую процентаэтих узлов, иесли компьютерпроверяетмиллион узловв секунду, тодля решенияэтой задачипотребовалосьбы более 2 миллионовлет. В задачах, для которыхалгоритм ветвейи границ выполняетсяслишком медленно, можно использоватьэвристическийподход.
Есликачество решенияне так важно, то приемлемымможет бытьрезультат, полученныйпри помощиэвристики. Внекоторыхслучаях точностьвходных данныхможет бытьнедостаточной.Тогда хорошееэвристическоерешение можетбыть таким жеправильным, как и теоретически«наилучшее»решение.
В предыдущемпримере методветвей и границиспользовалсядля выбораинвестиционныхвозможностей.Тем не менее, вложения могутбыть рискованными, и точные результатычасто заранеенеизвестны.Может быть, чтозаранее будетнеизвестенточный доходили даже стоимостьнекоторыхинвестиций.В этом случае, эффективноеэвристическоерешение можетбыть таким женадежным, каки наилучшеерешение, котороевы может вычислитьточно.

@Таблица8.2. Число узлов, проверенныхпри поискеметодами полногоперебора иветвей и границ

=======201

В этомразделе обсуждаютсяэвристики, которые полезныпри решениимногих сложныхзадач. ПрограммаHeurдемонстрируеткаждую из эвристик.Она также позволяетсравнить результаты, полученныепри помощиэвристик иметодов полногоперебора иветвей и границ.Введите значенияминимальнойи максимальнойстоимости идохода, а такжечисло позицийи полную стоимостьпортфеля всоответствующихполях областиParameters (Параметры), чтобы задатьпараметрысоздаваемыхданных. Затемвыберите алгоритмы, которые выхотите протестировать, и нажмите накнопку Go.Программавыведет полнуюстоимость идоход для наилучшегорешения, найденногопри помощикаждого изалгоритмов.Она также сортируетрешения помаксимальномуполученномудоходу и выводитвремя выполнениядля каждогоиз алгоритмов.Используйтеметод ветвейи границ толькодля небольшихзадач, а методполного переборатолько длязадач еще меньшегообъема.
На рис.8.8 показано окнопрограммы Heurпосле решениязадачи формированияпортфеля для20 позиций. ЭвристикиFixed1,Fixed2и NoChanges1, которыебудут вскореописаны, далинаилучшиеэвристическиерешения. Заметьте, что эти решениянемного хуже, чем точныерешения, которыеполучены прииспользованииметода ветвейи границ.Восхождениена холм
Эвристикавосхожденияна холм (hill climbing)вносит измененияв текущее решение, чтобы максимальноприблизитьего к цели. Этотпроцесс называетсявосхождениемна холм, таккак он похожна то, как заблудившийсяпутешественникпытается ночьюдобраться довершины горы.Даже если ужеслишком темно, чтобы еще можнобыло разглядетьчто то вдали, путешественникможет попытатьсядобраться довершины горы, постояннодвигаясь вверх.
Конечно, существуетвероятность, что путешественникзастрянет навершине меньшегохолма и не доберетсядо пика. Этапроблема всегдаможет возникатьпри использованииэтой эвристики.Алгоритм можетнайти решение, которое можетоказатьсялокально приемлемым, но это не обязательнонаилучшеевозможноерешение.
В задачео формированиипортфеля, цельзаключаетсяв том, чтобыподобрать наборпозиций, полнаястоимостькоторых непревышаетзаданногопредела, а общаяцена максимальна.На каждом шагеэвристикавосхожденияна холм будетвыбирать позицию, которая приноситнаибольшуюприбыль. Приэтом решениебудет все лучшесоответствоватьцели — получениюмаксимальнойприбыли.

@Рис.8.8. ПрограммаHeur

========202

Вначалепрограммадобавляет крешению позициюс максимальнойприбылью. Затемона добавляетследующуюпозицию смаксимальнойприбылью, еслипри этом полнаяцена еще остаетсяв допустимыхпределах. Онапродолжаетдобавлятьпозиции смаксимальнойприбылью дотех пор, покане останетсяпозиций, удовлетворяющихусловиям.
Длясписка инвестицийиз табл. 8.3, программавначале выбираетпозицию A, так как онадает максимальнуюприбыль — 9миллионовдолларов. Затемпрограммавыбирает следующуюпозицию C, которая даетприбыль 8 миллионов.В этот моментпотрачены уже93 миллиона из100, и программане может приобрестибольше позиций.Решение, полученноепри помощиэвристики, включает позицииA и C, имеет стоимость93 миллиона, иприносит 17 миллионовприбыли.

@Таблица8.3. Возможныеинвестиции

Эвристикавосхожденияна холм заполняетпортфель оченьбыстро. Еслипозиции изначальнобыли отсортированыв порядке убыванияприносимойприбыли, тосложность этогоалгоритмапорядка O(N).Программапросто перемещаетсяпо списку, добавляякаждую позицию, если под нееесть место.Даже если списокне упорядочен, то это алгоритмсо сложностьюпорядка O(N2).Это намноголучше, чем O(2N)шагов, которыетребуются дляполного переборавсех узлов вдереве. Для 20позиций этаэвристикатребует всегооколо 400 шагов, метод ветвейи границ —несколькотысяч, а полныйперебор — болеечем 2 миллиона.

PublicSub HillClimbing()
Dimi As Integer
Dimj As Integer
Dimbig_value As Integer
Dimbig_j As Integer

'Многократныйобход спискаи поиск следующей
'позиции, приносящейнаибольшуюприбыль,
'стоимостькоторой непревышаетверхней границы.
Fori = 1 To NumItems
big_value= 0
big_j= -1
Forj = 1 To NumItems
'Проверить, ненаходится лион уже
'в решении.
If(Not test_solution(j)) And _
(test_cost+ Items(j).Cost
(big_value
Then
big_value= Items(j).Profit
big_j= j
EndIf
Nextj

'Остановиться, если не найденапозиция,
'удовлетворяющаяусловиям.
Ifbig_j

test_cost= test_cost + Items(big_j).Cost
test_solution(big_j)= True
test_profit= test_profit + Items(big_j).Profit
Nexti
EndSub
Методнаименьшейстоимости
Стратегия, которая в каком тосмысле противоположнастратегиивосхожденияна холм, называетсястратегиейнаименьшейстоимости(least cost).Вместо тогочтобы на каждомшаге пытатьсямаксимальноприблизитьрешение к цели, можно попытатьсяуменьшитьстоимостьрешения, насколькоэто возможно.В примере сформированиемпортфеля, накаждом шагек решению добавляетсяпозиция с минимальнойстоимостью.
Этастратегияпытается поместитьв решение максимальновозможное числопозиций. Этобудет неплохимрешением, есливсе позицииимеют примерноодинаковуюстоимость. Еслидорогая позицияприносит большуюприбыль, то этастратегия можетупустить этувозможность, давая не лучшийиз возможныхрезультатов.
Дляинвестиций, показанныхв табл. 8.3, алгоритмнаименьшейстоимостиначинает сдобавленияк решению позицииE со стоимостью23 миллиона долларов.Затем он выбираетпозицию D, стоящую 27 миллионов, и затем позициюC со стоимостью30 миллионов. Вэтой точкеалгоритм ужепотратил 80 миллионовиз 100 возможных, поэтому большеон не можетвыбрать ниодной позиции.
Эторешение имеетстоимость 80миллионов идает 18 миллионовприбыли. Этона миллионлучше, чем решениедля эвристикивосхожденияна холм, но стратегиянаименьшейстоимости невсегда даетлучшее решение, чем восхождениена холм. Какаяиз эвристикдает лучшиерезультаты, зависит отзначений входныхданных.
Структурапрограммы, реализующейэвристикунаименьшейстоимости, почти идентичнаструктурепрограммы дляэвристикивосхожденияна холм. Единственноеразличие междуними заключаетсяв выборе следующейпозиции длядобавленияк решению. Эвристиканаименьшейстоимостивыбирает позициюс минимальнойценой; методвосхожденияна холм выбираетпозицию смаксимальнойприбылью. Таккак эти дваметода оченьпохожи, онивыполняютсяза одинаковоевремя. Еслипозиции упорядоченысоответствующимобразом, то обаалгоритмавыполняютсяза время порядкаO(N). Еслипозиции расположеныслучайнымобразом, то обавыполняютсяза время порядкаO(N2).

========203-204

Таккак код на языкеVisual Basic дляэтих двух эвристикочень похож, то мы приводимтолько строки, в которых происходитвыбор очереднойпозиции.

If(Not test_solution(j)) And _
(test_cost+ Items(j).Cost
(small_cost> Items(j).Cost)
Then
small_cost= Items(j).Cost
small_j= j
EndIf
Сбалансированнаяприбыль
Стратегиявосхожденияна холм не учитываетстоимостьдобавляемыхпозиций. Онавыбирает позициис максимальнойприбылью, дажеесли их стоимостьвелика. Стратегиянаименьшейстоимости неучитываетприносимуюпозицией прибыль.Она выбираетпозиции с низкойстоимостью, даже если ониприносят малоприбыли.
Эвристикасбалансированнойприбыли (balancedprofit) сравниваетпри выборестоимостьпозиций и приносимуюими прибыль.На каждом шагеэвристикавыбирает позициюс наибольшимотношениемприбыль стоимость.
В табл.8.4 приведеныте же данные, что и в табл.8.3, но в ней добавленаеще одна колонкас отношениемприбыль стоимость.При этом подходевначале выбираетсяпозиция C, так как онаимеет максимальноесоотношениеприбыль стоимость —0,27. Затем к решениюдобавляетсяпозиция Dс отношением0,26, и позиция Bс отношением0,20. В этой точке, будет потрачено92 миллиона из100 возможных, и в решениенельзя будетдобавить большени одной позиции.
Решениебудет иметьстоимость 92миллиона идавать 22 миллионаприбыли. Этона 4 миллионалучше, чем решениес наименьшейстоимостьюи на 5 миллионовлучше, чем решениеметодом восхожденияна холм. В этомслучае, этобудет такженаилучшимвозможнымрешением, и еготакже можнонайти полнымперебором илиметодом ветвейи границ. Методсбалансированнойприбыли темне менее, являетсяэвристическим, поэтому он необязательнонаходит наилучшеевозможноерешение. Ончасто находитлучшее решение, чем методынаименьшейстоимости ивосхожденияна холм, но этоне обязательнотак.

@Таблица8.4. Возможныеинвестициис соотношениемприбыль стоимость

=========205

Структурапрограммы, реализующейэвристикусбалансированнойприбыли, почтиидентичнаструктурепрограмм длявосхожденияна холм и наименьшейстоимости.Единственноеотличие заключаетсяв методе выбораследующейпозиции, котораядобавляетсяк решению:

If(Not test_solution(j)) And _
(test_cost+ Items(j).Cost
(good_ratio
Then
good_ratio= Items(j).Profit / CDbl(Items(j).Cost)
good_j= j
EndIf
Случайныйпоиск
Случайныйпоиск (randomsearch) выполняетсяв соответствиисо своим названием.На каждом шагеалгоритм добавляетслучайнуюпозицию, котораяудовлетворяетверхнему ограничениюна суммарнуюстоимостьпозиций в портфеле.Этот методпоиска такженазываетсяметодом Монте Карло(Monte Carlosearch или MonteCarlo simulation).
Таккак маловероятно, что случайновыбранноерешение окажетсянаилучшим, необходимомногократноповторять этотпоиск, чтобыполучить приемлемыйрезультат. Хотяможет показаться, что вероятностьнахожденияхорошего решенияпри этом мала, этот методиногда даетудивительнохорошие результаты.В зависимостиот значенийданных и числапроверенныхслучайныхрешений результат, полученныйпри помощи этойэвристики, часто оказываетсялучше, чем вслучае примененияметодов восхожденияна холм илинаименьшейстоимости.
Преимуществослучайногопоиска состоиттакже и в том, что этот методлегок в пониманиии реализации.Иногда сложнопредставить, как реализоватьрешение задачипри помощиэвристик восхожденияна холм, наименьшейстоимости, илисбалансированногодохода, но всегдапросто выбиратьрешения случайнымобразом. Дажедля очень сложныхпроблем, случайныйпоиск являетсяпростым эвристическимметодом.
ПодпрограммаRandomSearchв программеHeurиспользуетфункцию AddToSolutionдля добавленияк решению случайнойпозиции. Этафункция возвращаетзначение True, если она неможет найтипозицию, котораяудовлетворяетусловиям, иFalseв другом случае.ПодпрограммаRandomSearchвызывает функциюAddToSolutionдо тех пор, покабольше нельзядобавить ниодной позиции.

PublicSub RandomSearch()
Dimnum_trials As Integer
Dimtrial As Integer
Dimi As Integer

'Сделать несколькопопыток и выбратьнаилучшийрезультат.
num_trials= NumItems ' ИспользоватьN попыток.
Fortrial = 1 To num_trials
'Случайный выборпозиций, покаэто возможно.
DoWhile AddToSolution()
'Всю работувыполняетфункция AddToSolution.
Loop

'Определить, лучше ли эторешение, чемпредыдущее.
Iftest_profit > best_profit Then
best_profit= test_profit
best_cost= test_cost
Fori = 1 To NumItems
best_solution(i)= test_solution(i)
Nexti
EndIf

'Сбросить пробноерешение и сделатьеще одну попытку.
test_profit= 0
test_cost= 0
Fori = 1 To NumItems
test_solution(i)= False
Nexti
Nexttrial
EndSub

PrivateFunction AddToSolution() As Boolean
Dimnum_left As Integer
Dimj As Integer
Dimselection As Integer

'Определить, сколько осталосьпозиций, которые
'удовлетворяютограничениюмаксимальнойстоимости.
num_left= 0
Forj = 1 To NumItems
If(Not test_solution(j)) And _
(test_cost+ Items(j).Cost
Thennum_left = num_left + 1
Nextj

'Остановиться, если нельзянайти новуюпозицию.
Ifnum_left
AddToSolution= False
ExitFunction
EndIf

'Выбрать случайнуюпозицию.
selection= Int((num_left) * Rnd + 1)

'Найти случайновыбраннуюпозицию.
Forj = 1 To NumItems
If(Not test_solution(j)) And _
(test_cost+ Items(j).Cost
Then
selection= selection — 1
Ifselection
EndIf
Nextj
    продолжение
--PAGE_BREAK--
test_profit= test_profit + Items(j).Profit
test_cost= test_cost + Items(j).Cost
test_solution(j)= True

AddToSolution= True
EndFunction
Последовательноеприближение
Ещеодна стратегиязаключаетсяв том, чтобыначать со случайногорешения и затемделать последовательныеприближения(incremental improvements).Начав со случайновыбранногорешения, программаделает случайныйвыбор. Еслиновое решениелучше предыдущего, программазакрепляетизменения ипродолжаетпроверку другихслучайныхизменений. Еслиизменение неулучшает решение, программаотбрасываетего и делаетновую попытку.
Длязадачи формированияпортфеля особеннопросто порождатьслучайныеизменения.Программапросто выбираетслучайнуюпозицию изпробного решения, и удаляет ееиз текущегорешения. Оназатем сновадобавляетслучайныепозиции в решениедо тех пор, покаони помещаются.Если удаленнаяпозиция имелаочень высокуюстоимость, тона ее местопрограмма можетпоместитьнесколькопозиций.Моментостановки
Естьнесколькохороших способовопределитьмомент, когдаследует прекратитьслучайныеизменения. Дляпроблемы с Nпозициями, можно выполнитьN или N2случайныхизменений, перед тем, какостановиться.

=====206-208

В программеHeurэтот подходреализованв процедуреMakeChangesFixed.Она выполняетопределенноечисло случайныхизменений срядом случайныхпробных решений:

PublicSub MakeChangesFixed(K As Integer, num_trials As Integer, num_changesAs Integer)
Dimtrial As Integer
Dimchange As Integer
Dimi As Integer
Dimremoval As Integer

Fortrial = 1 To num_trials
'Найти случайноепробное решениеи использоватьего
'в качественачальнойточки.
DoWhile AddToSolution()
'All the work is done by AddToSolution.
Loop

'Начать с этогопробного решения.
trial_profit= test_profit
trial_cost= test_cost
Fori = 1 To NumItems
trial_solution(i)= test_solution(i)
Nexti

Forchange = 1 To num_changes
'Удалить K случайныхпозиций.
Forremoval = 1 To K
RemoveFromSolution
Nextremoval

'Добавить максимальновозможное
'число позиций.
DoWhile AddToSolution()
'All the work is done by AddToSolution.
Loop

'Если это улучшаетпробное решение, сохранить его.
'Иначе вернутьпрежнее значениепробного решения.
Iftest_profit > trial_profit Then
'Сохранитьизменения.
trial_profit= test_profit
trial_cost= test_cost
Fori = 1 To NumItems
trial_solution(i)= test_solution(i)
Nexti
Else
'Сбросить пробноерешение.
test_profit= trial_profit
test_cost= trial_cost
Fori = 1 To NumItems
test_solution(i)= trial_solution(i)
Nexti
EndIf
Nextchange

'Если пробноерешение лучшепредыдущего
'наилучшегорешения, сохранитьего.
Iftrial_profit > best_profit Then
best_profit= trial_profit
best_cost= trial_cost
Fori = 1 To NumItems
best_solution(i)= trial_solution(i)
Nexti
EndIf

'Сбросить пробноерешение для
'следующейпопытки.
test_profit= 0
test_cost= 0
Fori = 1 To NumItems
test_solution(i)= False
Nexti
Nexttrial
EndSub

PrivateSub RemoveFromSolution()
Dimnum_in_solution As Integer
Dimj As Integer
Dimselection As Integer

'Определитьчисло позицийв решении.
num_in_solution= 0
Forj = 1 To NumItems
Iftest_solution(j) Then num_in_solution = num_in_solution + 1
Nextj
Ifnum_in_solution
'Выбрать случайнуюпозицию.
selection= Int((num_in_solution) * Rnd + 1)
'Найти случайновыбраннуюпозицию.
Forj = 1 To NumItems
Iftest_solution(j) Then
selection= selection — 1
Ifselection
EndIf
Nextj

'Удалить позициюиз решения.
test_profit= test_profit — Items(j).Profit
test_cost= test_cost — Items(j).Cost
test_solution(j)= False
EndSub

======209-210

Другаястратегиязаключаетсяв том, чтобывносить изменениядо тех пор, поканесколькопоследовательныхизменений неприносят улучшений.Для задачи сN позициями, программа можетвносить изменениядо тех пор, покав течение N измененийподряд улучшенийне будет.
Этастратегияреализованав подпрограммеMakeChangesNoChangeпрограммы Heur.Она повторяетпопытки до техпор, пока определенноечисло последовательныхпопыток не дастникаких улучшений.Для каждойпопытки онавносит случайныеизменения впробное решениедо тех пор, покапосле определенногочисла измененийне наступитникаких улучшений.

PublicSub MakeChangesNoChange(K As Integer, _
max_bad_trialsAs Integer, max_non_changes As Integer)
Dimi As Integer
Dimremoval As Integer
Dimbad_trials As Integer ' Неэффективныхпопытокподряд.
Dimnon_changes As Integer ' Неэффективныхизмененийподряд.

'Повторятьпопытки, покане встретитсяmax_bad_trials
'попыток подрядбез улучшений.
bad_trials= 0
Do
'Выбрать случайноепробное решениедля
'использованияв качественачальнойточки.
DoWhile AddToSolution()
'All the work is done by AddToSolution.
Loop

'Начать с этогопробного решения.
trial_profit= test_profit
trial_cost= test_cost
Fori = 1 To NumItems
trial_solution(i)= test_solution(i)
Nexti

'Повторять, покаmax_non_changes изменений
'подряд не дастулучшений.
non_changes= 0
DoWhile non_changes
'Удалить K случайныхпозиций.
Forremoval = 1 To K
RemoveFromSolution
Nextremoval

'Вернуть максимальновозможное числопозиций.
DoWhile AddToSolution()
'All the work is done by
'AddToSolution.
Loop

'Если это улучшаетпробное значение, сохранить его.
'Иначе вернутьпрежнее значениепробного решения.
Iftest_profit > trial_profit Then
'Сохранитьулучшение.
trial_profit= test_profit
trial_cost= test_cost
Fori = 1 To NumItems
trial_solution(i)= test_solution(i)
Nexti
non_changes= 0 ' This was a good change.
Else
'Reset the trial.
test_profit= trial_profit
test_cost= trial_cost
Fori = 1 To NumItems
test_solution(i)= trial_solution(i)
Nexti
non_changes= non_changes + 1 ' Плохоеизменение.
EndIf
Loop 'Продолжитьпроверку случайныхизменений.

'Если эта попыткалучше, чем предыдущеенаилучшее
'решение, сохранитьего.
Iftrial_profit > best_profit Then
best_profit= trial_profit
best_cost= trial_cost
Fori = 1 To NumItems
best_solution(i)= trial_solution(i)
Nexti
bad_trials= 0 ' Хорошаяпопытка.
Else
bad_trials= bad_trials + 1 ' Плохаяпопытка.
EndIf

'Сбросить тестовоерешение дляследующейпопытки.
test_profit= 0
test_cost= 0
Fori = 1 To NumItems
test_solution(i)= False
Nexti
LoopWhile bad_trials
EndSub
Локальныеоптимумы
Еслипрограммазаменяет случайновыбраннуюпозицию в пробномрешении, томожет встретитьсярешение, котороеона не можетулучшить, нокоторое приэтом не будетнаилучшим извозможныхрешений. Например, рассмотримсписок инвестиций, приведенныйв табл. 8.5.
Предположим, что алгоритмслучайно выбралпозиции Aи B в качественачальногорешения. Егостоимость будетравно 90 миллионамдолларов, и онопринесет 17 миллионовприбыли.
Еслипрограммаудалит позицииA и B, тостоимостьрешения будетвсе еще настольковелика, чтопрограммасможет добавитьвсего лишь однупозицию к решению.Так как наибольшуюприбыль приносятпозиции Aи B, то заменаих другимипозициямиуменьшит суммарнуюприбыль. Случайноеудаление однойпозиции изэтого решенияникогда неприведет кулучшениюрешения.
Наилучшеерешение содержитпозиции C,D и E. Егополная стоимостьравно 98 миллионамдолларов исуммарнаяприбыль составляет18 миллионовдолларов. Чтобынайти это решение, алгоритму быпонадобилосьудалить изрешения сразуобе позицииA и B изатем добавитьна их местоновые позиции.
Решениятакого типа, для которыхнебольшиеизменениярешения немогут улучшитьего, называютсялокальнымоптимумом(local optimum).Можно использоватьдва способадля того, чтобыпрограмма незастревалав локальномоптимуме, имогла найтиглобальныйоптимум (globaloptimum).

@Таблица8.5. Возможныеинвестиции

=============213

Во первых, можно изменитьпрограмму так, чтобы она удалялаболее однойпозиции вовремя случайныхизменений. Вэтом примере, программа моглабы найти правильноерешение, еслибы она одновременноудаляла бы подве случайновыбранныхпозиции. Темне менее, длязадач большегоразмера, удалениядвух позицийможет бытьнедостаточно.Программе можетпонадобитьсяудалять три, четыре, илибольше позиций.
Второй, более простойспособ заключаетсяв том, чтобыделать большепопыток, начинаяс разных начальныхрешений. Некоторыеиз начальныхрешений будутприводить клокальнымоптимумам, ноодно из нихпозволит достичьглобальногооптимума.
ПрограммаHeurдемонстрируеттри стратегиипоследовательныхприближений.При выбореметода Fixed1 (Фиксированный1) делается Nпопыток. Вовремя каждойпопытки выбираетсяслучайно решение, которое программазатем пытаетсяулучшить за2 * N попыток, случайно удаляяпо одной позиции.
Привыборе эвристикиFixed 2 (Фиксированный2)делается всегоодна попытка.При этом программавыбирает случайноерешение и пытаетсяулучшить его, случайнымобразом удаляяпо одной позициидо тех пор, покав течение Nпоследовательныхизменений небудет никакихулучшений.
Привыборе эвристикиNo Changes1 (Без изменений1) программавыполняетпопытки до техпор, пока послеN последовательныхпопыток небудет никакихулучшений. Вовремя каждойпопытки программавыбирает случайноерешение и затемпытается улучшитьего, случайнымобразом удаляяпо одной позициидо тех пор, покав течение Nпоследовательныхизменений небудет никакихулучшений.
Привыборе эвристикиNo Changes2 (Без изменений2)делается однапопытка. Приэтом программавыбирает случайноерешение и пытаетсяулучшить его, случайнымобразом удаляяпо две позициидо тех пор, покав течение Nпоследовательныхизменений небудет никакихулучшений.
Названияэвристик и ихописания приведеныв табл. 8.6.Алгоритм«отжига»
Метод отжига(simulated annealing)ведет своеначало изтермодинамики.При отжигеметалла оннагреваетсядо высокойтемпературы.Молекулы внагретом металлесовершаютбыстрые колебания, а при медленномостывании ониначинаютрасполагатьсяупорядоченно, образуя кристаллы.При этом молекулыпостепеннопереходят всостояние сминимальнойэнергией.

@Таблица8.6. Стратегиипоследовательныхприближений

===========214

Примедленномостыванииметалла, соседниекристаллысливаются другс другом. Молекулыв одном из кристалловпокидают состояниес минимальнойэнергией ипринимаютпорядок молекулв другом кристалле.Энергия получившегосякристаллабольшего размерабудет меньше, чем сумма энергийдвух исходныхкристаллов.Если охлаждениепроисходитдостаточномедленно, токристаллыстановятсяочень большими.Окончательноераспределениемолекул представляетсостояние сочень низкойэнергией, иметалл при этомбудет оченьтвердым.
Начинаяс состоянияс высокой энергией, молекулы вконце концовдостигаютсостояния сочень низкойэнергией. Напути к конечномуположению, онипроходят множестволокальныхминимумовэнергии. Каждоесочетаниекристалловобразует локальныйминимум. Кристаллымогут объединятьсядруг с другомтолько за счетвременногоповышенияэнергии системы, чтобы затемперейти к состояниюс меньшей энергией.
Методотжига используетаналогичныйподход дляпоиска наилучшегорешения задачи.Во время поискарешения программой, она может застрятьв локальномоптимуме. Чтобыизбежать этого, программа времяот временивносит в решениеслучайныеизменения, дажеесли очередноеизменение ине приводитк мгновенномуулучшениюрезультата.Это может помочьпрограмме выйтииз локальногооптимума иотыскать лучшеерешение. Еслиэто изменениене ведет к лучшемурешению, товероятно, черезнекоторое времяпрограмма егоотбросит.
Чтобыэти измененияне возникалипостоянно, алгоритм изменяетвероятностьвозникновенияслучайныхизменений современем. ВероятностьP возникновенияодного из подобныхизмененийопределяетсяформулойP = 1 / Exp(E/ (k * T)), гдеE — увеличение«энергии»системы, k —некотораяпостоянная, и T — переменная, соответствующая«температуре».
Вначалетемпературадолжна бытьвысокой, поэтомуи вероятностьизмененийP = 1 / Exp(E/ (k * T)) такжедостаточновелика. Иначеслучайныеизменения моглибы никогда невозникнуть.С течениемвремени значениепеременнойT постепенноснижается, ивероятностьслучайныхизменений такжеуменьшается.После того, какмодель дойдетдо точки, в которойона никакиеизменения несмогут улучшитьрешение, итемператураT станетдостаточнонизкой, чтобывероятностьслучайныхизменений быламала, алгоритмзаканчиваетработу.
Длязадачи о формированияпортфеля, вкачестве прибавки«энергии» Eвыступаетуменьшениеприбыли решения.Например, приудалении позиции, которая даетприбыль 10 миллионов, и замене ее напозицию, котораяприносит 7 миллионовприбыли, энергия, добавленнаяк системе, будетравна 3.
Заметьте, что если энергиявелика, товероятностьизмененийP = 1 / Exp(E/ (k * T)) мала, поэтому вероятностьбольших измененийниже.
Алгоритмотжига в программеHeurустанавливаетзначение постояннойk равнымразнице междунаибольшейи наименьшейприбылью возможныхинвестиций.НачальнаятемператураT задаетсяравной 0,75. Послевыполненияопределенногочисла случайныхизменений, температураT уменьшаетсяумножениемна постоянную0,95.

=========215

PublicSub AnnealTrial(K As Integer, max_non_changes As Integer, _
max_back_slipsAs Integer)
ConstTFACTOR = 0.95

Dimi As Integer
Dimnon_changes As Integer
Dimt As Double
Dimmax_profit As Integer
Dimmin_profit As Integer
Dimdoit As Boolean
Dimback_slips As Integer

'Найти позициюс минимальнойи максимальнойприбылью.
max_profit= Items(1).Profit
min_profit= max_profit
Fori = 2 To NumItems
Ifmax_profit
Ifmin_profit > Items(i).Profit Then min_profit = Items(i).Profit
Nexti

t= 0.75 * (max_profit — min_profit)
back_slips= 0
'Выбрать случайноепробное решение
'в качественачальнойточки.
DoWhile AddToSolution()
'Вся работавыполняетсяв процедуреAddToSolution.
Loop

'Использоватьв качествепробного решения.
best_profit= test_profit
best_cost= test_cost
Fori = 1 To NumItems
best_solution(i)= test_solution(i)
Nexti

'Повторять, покав течениеmax_non_changes изменений
'подряд не будетулучшений.
non_changes= 0
DoWhile non_changes
'Удалить случайнуюпозицию.
Fori = 1 To K
RemoveFromSolution
Nexti
'Добавить максимальновозможное числопозиций.
DoWhile AddToSolution()
'Вся работавыполняетсяв процедуреAddToSolution.
Loop
'Если изменениеулучшает пробноерешение, сохранитьего.
'Иначе вернутьпрежнее значениерешения.
Iftest_profit > best_profit Then
doit= True
ElseIftest_profit
doit= (Rnd
back_slips= back_slips + 1
Ifback_slips > max_back_slips Then
back_slips= 0
t= t * TFACTOR
EndIf
Else
doit= False
EndIf
Ifdoit Then
'Сохранитьулучшение.
best_profit= test_profit
best_cost= test_cost
Fori = 1 To NumItems
best_solution(i)= test_solution(i)
Nexti
non_changes= 0 ' Хорошееизменение.
Else
'Reset the trial.
test_profit= best_profit
test_cost= best_cost
Fori = 1 To NumItems
test_solution(i)= best_solution(i)
Nexti
non_changes= non_changes + 1 ' Плохоеизменение.
EndIf
Loop 'Продолжитьпроверку случайныхизменений.
EndSub
Сравнениеэвристик
Различныеэвристикипо разномуведут себя вразличныхзадачах. Длязадачи о формированиипортфеля, эвристикасбалансированнойприбыли работаетдостаточнохорошо, учитываяее простоту.Стратегиипоследовательногоприближенияобычно даютсравнимыерезультаты, но для большихзадач их выполнениезанимает намногобольше времени.Для другихзадач наилучшейможет бытькакая либодругая эвристика, в том числе изтех, которыене обсуждалисьв этой главе.

========216-217

Эвристическиеметоды обычновыполняютсябыстрее, чемметод ветвейи границ. Некоторыеиз них, напримерметоды восхожденияна холм, наименьшейстоимости исбалансированнойприбыли, выполняютсяочень быстро, так как онирассматриваюттолько одновозможноерешение. Онивыполняютсянастолькобыстро, чтоимеет смыслвыполнить ихвсе по очереди, и затем выбратьнаилучшее изтрех полученныхрешений. Этоне гарантируеттого, что эторешение будетнаилучшим, нодает некоторуюуверенность, что оно окажетсядостаточнохорошим.Другиесложные задачи
Существуетмножество оченьсложных задач, большинствоиз которых неимеет решенийс полиномиальнойвычислительнойсложностью.Другими словами, не существуеталгоритмов, которые решалибы эти задачиза время порядкаO(NC)для любых постоянныхC, даже заO(N1000).
В следующихразделах краткоописаны некоторыеиз этих задач.В них такжепоказано, почемуони являютсясложными вобщем случаеи насколькобольшим можетоказатьсядерево решенийзадачи. Вы можетепопробоватьприменить методветвей и границили эвристикидля решениянекоторых изэтих задач.Задачао выполнимости
Еслиимеется логическоеутверждение, например “(AAndNotB)OrC”, то существуютли значенияпеременныхA,Bи C, при которыхэто утверждениеистинно? В данномпримере легкоувидеть, чтоутверждениеистинно, еслиA= true,B= falseи C= false.Для более сложныхутверждений, содержащихсотни переменных, бывает достаточносложно определить, может ли бытьутверждениеистинным.
Припомощи метода, похожего натот, которыйиспользовалсяпри решениизадачи о формированиипортфеля, можнопростроитьдерево решенийдля задачи овыполнимости(satisfiability problem).Каждая ветвьдерева будетсоответствоватьрешению о присвоениипеременнойзначения trueили false.Например, леваяветвь, выходящаяиз корня, соответствуетзначению первойпеременнойtrue.
Еслив логическомвыражении Nпеременных, то дерево решенийпредставляетсобой двоичноедерево высотойN + 1. Это деревоимеет 2Nлистьев, каждыйиз которыхсоответствуетразной комбинациизначений переменных.
В задачео формированиипортфеля можнобыло использоватьметод ветвейи границ длятого, чтобыизбежать поискав большей частидерева. В задачео выполнимостивыражение либоистинно, либоложно. При этомнельзя получитьчастичноерешение, котороеможно использоватьдля отсеченияпутей в дереве.
Нельзятакже использоватьэвристики дляпоиска приблизительногорешения длязадачи о выполнимости.Любое значениепеременных, полученноепри помощиэвристики, будет делатьвыражениеистинным илиложным. В математическойлогике не существуеттакого понятия, как приближенноерешение.
Из занеприменимостиэвристик именьшей эффективностиметода ветвейи границ, задачао выполнимостиобычно являетсяочень сложнойи решаетсятолько в случаенебольшогоразмера задачи.Задачао разбиении
Еслизадано множествоэлементов созначениямиX1, X2,…, XN, то существуетли способ разбитьего на дваподмножества, так чтобы суммазначений всехэлементов вкаждом из подмножествбыла одинаковой? Например, еслиэлементы имеютзначения 3, 4, 5 и6, то их можноразбить на дваподмножества{3, 6} и {4, 5}, сумма значенийэлементов вкаждом из которыхравна 9.
Чтобысмоделироватьэту задачу припомощи дерева, предположим, что ветвямсоответствуетпомещениеэлемента в одноиз двух подмножеств.Левая ветвь, выходящая изкорневого узла, соответствуетпомещениюпервого элементав первое подмножество, а правая ветвь —во второеподмножество.
Есливсего существуетN элементов, то дерево решениебудет представлятьсобой двоичноедерево высотойN + 1. Оно будетсодержать 2Nлистьев и 2N+1узлов. Каждыйлист соответствуетодному из вариантовразмещенияэлементов вдвух подмножествах.
Прирешении этойзадачи можноприменить методветвей и границ.При рассмотрениичастичныхрешений задачиможно отслеживать, насколькоразличаютсясуммарныезначения элементовв двух подмножествах.Если в какой томомент суммарноезначение элементовдля одного изподмножествнастолькоменьше, чем длядругого, чтодобавлениевсех оставшихсяэлементов непозволяетизменить этосоотношение, то нет смыслапродолжатьдвижение внизпо этой ветви.
Также, как и в случаес задачей овыполнимости, для задачи оразбиении(partition problem)нельзя получитьприближенноерешение. В результатевсегда должнополучитьсядва подмножества, суммарноезначение элементовв которых будетили не будетодинаковым.Это означает, что для решенияэтой задачинеприменимыэвристики, которые использовалисьдля решениязадачи о формированиипортфеля.
Задачуо разбиенииможно обобщитьследующимобразом: еслиимеется множествоэлементов созначениямиX1, X2,…, XN, как разбитьего на дваподмножества, чтобы разницасуммы значенийэлементов вдвух подмножествахбыла минимальной?
Получитьточное решениеэтой задачитруднее, чемдля исходнойзадачи о разбиении.Если бы существовалпростой способрешения задачив общем случае, то его можнобыло бы использоватьдля решенияисходной задачи.В этом случаеможно было быпросто найтидва подмножества, удовлетворяющихусловиям, азатем проверить, совпадают лисуммы значенийэлементов вних.
Длярешения общегослучая задачиможно использоватьметод ветвейи границ, примернотак же, как ониспользовалсядля решениячастного случаязадачи, чтобыизбежать поискапо всему дереву.Можно такжеиспользоватьпри этом эвристическийподход. Например, можно проверятьэлементы впорядке убыванияих значения, помещая очереднойэлемент вподмножествос меньшей суммойзначений элементов.Также можнобыло бы легкоиспользоватьслучайныйпоиск, методпоследовательныхприближений, или метод отжигадля поискаприближенногорешения этогообщего случаязадачи.Задачапоиска Гамильтоновапути
Еслизадана сеть, то Гамильтоновымпутем (Hamiltonianpath) для нееназываетсяпуть, обходящийвсе узлы в сетитолько одинраз и затемвозвращающийсяв начальнуюточку.
На рис.8.9 показананебольшая сетьи Гамильтоновпуть для нее, нарисованныйжирной линией.
Задачапоиска Гамильтоновапути формулируетсятак: если заданасеть, существуетли для нееГамильтоновпуть?
    продолжение
--PAGE_BREAK--
==============219

@Рис.8.9. Гамильтоновпуть

Таккак Гамильтоновпуть обходитвсе узлы в сети, то не нужноопределять, какие из узловпопадают внего, а какиенет. Необходимоустановитьтолько порядок, в котором ихнужно обойтидля созданияГамильтоновапути.
Длямоделированияэтой задачипри помощидерева, предположим, что ветвисоответствуютвыбору следующегоузла в пути.Корневой узелтогда будетсодержать Nветвей, соответствующихначалу путив каждом из Nузлов. Каждыйиз узлов первогоуровня будетиметь N –1 ветвей, по однойветви для каждогоиз оставшихсяN – 1 узлов.Узлы на следующемуровне деревабудут иметьN – 2 ветвей, и так далее.Нижний уровеньдерева будетсодержать N! листьев, соответствующихN! возможныхпутей. Всегов дереве будетнаходитьсяпорядка O(N!)узлов.
Каждыйлист соответствуетГамильтоновупути, но числолистьев можетбыть разнымдля различныхсетей. Если дваузла в сети несвязаны другс другом, то вдереве будутотсутствоватьветви, которыесоответствуютпереходам междуэтими двумяузлами. Этоуменьшает числопутей в деревеи соответственно, число листьев.
Также, как и в задачахо выполнимостии о разбиении, для задачипоиска Гамильтоновапути нельзяполучить приближенноерешение. Путьможет либоявлятьсяГамильтоновым, либо нет. Этоозначает, чтоэвристическийподход и методветвей и границне помогут припоиске Гамильтоновапути. Что ещехуже, дереворешений длязадачи поискаГамильтоновапути содержитпорядка O(N!)узлов. Это намногобольше, чемпорядка O(2N)узлов, которыесодержат деревьярешений длязадач о выполнимостии разбиении.Например, 220примерно равно1 * 10 6, тогда как20! составляетоколо 2,4 * 1018 —в миллион разбольше. Из заочень большогоразмера дереварешений задачинахожденияГамильтоновапути, поиск внем можно выполнитьтолько длязадач оченьнебольшогоразмера.Задачакоммивояжера
Задачакоммивояжера(traveling salesmanproblem) тесносвязана с задачейпоиска Гамильтоновапути. Она формулируетсятак: найти самыйкороткий Гамильтоновпуть для сети.

========220

Этазадача имеетпримерно такоеже отношениек задаче поискаГамильтоновапути, как обобщенныйслучай задачио разбиениик простой задачео разбиении.В первом случаевозникаетвопрос о существованиирешения. Вовтором — какоеприближенноерешение будетнаилучшим. Еслибы существовалопростое решениевторой задачи, то его можнобыло бы использоватьдля решенияпервого вариантазадачи.
Обычнозадача коммивояжеравозникаеттолько в сетях, содержащихбольшое числоГамильтоновыхпутей. В типичномпримере, коммивояжерутребуетсяпосетить несколькоклиентов, используякратчайшиймаршрут. В случаеобычной сетиулиц, любые дветочки в сетисвязаны междусобой, поэтомулюбой маршрутпредставляетсобой Гамильтоновпуть. Задачазаключаетсяв том, чтобынайти самыйкороткий изних.
Так жекак и в случаепоиска Гамильтоновапути, дереворешений дляэтой задачисодержит порядкаO(N!) узлов.Так же, как и вобобщеннойзадаче о разбиении, для отсеченияветвей дереваи ускоренияпоиска решениязадач среднихразмеров можноиспользоватьметод ветвейи границ.
Существуеттакже несколькохороших эвристическихметодов последовательныхприближенийдля задачикоммивояжера.Например, использованиестратегии парпутей, при которойперебираютсяпары отрезковмаршрута. Программапроверяет, станет ли маршруткороче, еслиудалить паруотрезков изаменить ихдвумя новым, так чтобы маршрутпри этом оставалсязамкнутым. Нарис. 8.10 показанокак изменяетсямаршрут, еслиотрезки X1и X2 заменитьотрезками Y1и Y2. Аналогичныестратегиипоследовательныхприближенийрассматриваютзамену трехили более отрезковпути одновременно.
Обычнотакие шагипоследовательногоприближенияповторяютсямногократноили до тех пор, пока не будутпроверены всевозможные парыотрезков пути.После того, какдальнейшиешаги не приводятк улучшениям, можно сохранитьрезультат иначать работуснова, случайнымобразом выбравдругой исходныймаршрут. Послепроверки достаточнобольшого числаразличныхслучайныхисходных маршрутов, вероятно будетнайден достаточнокороткий путь.Задачао пожарных депо
Задачао пожарных депо(firehouse problem)формулируетсятак: если заданасеть, некотороечисло F, ирасстояниеD, то существуетли способ размеситьF пожарныхдепо такимобразом, чтобывсе узлы сетинаходилисьне дальше, чемна расстоянииD от ближайшегопожарного депо?

@Рис.8.10. Последовательноеприближениепри решениизадачи коммивояжера

========221

Этузадачу можносмоделироватьпри помощидерева решений, в котором каждаяветвь определяетместоположениесоответствующегопожарного депов сети. Корневойузел будетиметь Nветвей, соответствующихразмещениюпервого пожарногодепо в одномиз N узловсети. Узлы наследующемуровне деревабудут иметьN – 1 ветвей, соответствующихразмещениювторого пожарногодепо в одномиз оставшихсяN – 1 узлов.Если всегосуществуетF пожарныхдепо, то высотадерева решенийбудет равнаF, и оно будетсодержатьпорядка O(NF)узлов. В деревебудет N * (N– 1) * … * (N – F)листьев, соответствующихразным вариантамразмещенияпожарных депов сети.
Также, как и в задачахо выполнимости, разбиении, ипоиске Гамильтоновапути, в этойзадаче нужнодать положительныйили отрицательныйответ на вопрос.Это означает, что при проверкедерева решенийнельзя использоватьчастичные илиприближенныерешения.
Можно, тем не менее, использоватьразновидностьметода ветвейи границ, еслина ранних этапахрешения определить, какие из вариантовразмещенияпожарных депоне приводятк решению. Например, бессмысленнопомещать очередноедепо междудвумя другими, расположеннымирядом. Если всеузлы на расстоянииD от новогопожарного депоуже находятсяв пределахэтого расстоянияот другогодепо, значит, новое депонужно поместитьв какое тодругое место.Тем не менее, такого родавычислениятакже отнимаютдостаточномного времени, и задача всееще остаетсяочень сложной.
Также, как и длязадач о разбиениии поиске Гамильтоновапути, существуетобобщенныйслучай задачио пожарныхдепо. В обобщенномслучае задачаформулируетсятак: если заданасеть и некотороечисло F, вкаких узлахсети нужнопоместить Fпожарных депо, чтобы наибольшеерасстояниеот любого узладо пожарногодепо быломинимальным?
Также, как и обобщенныхслучаях другихзадач, для поискачастичногои приближенногорешений этойзадачи можноиспользоватьметод ветвейи границ иэвристическийподход. Этонесколькоупрощает проверкудерева решений.Хотя дереворешений всееще остаетсяогромным, можнопо крайней меренайти приблизительныерешения, дажеесли они и неявляются наилучшими.Краткаяхарактеристикасложных задач
Во времячтения предыдущихпараграфоввы могли заметить, что существуетдва вариантамногих сложныхзадач. Первыйвариант задачизадает вопрос:«Существуетли решениезадачи, удовлетворяющееопределеннымусловиям?».Второй, болееобщий случайдает ответ навопрос: «Какоерешение задачибудет наилучшим?»
Обезадачи при этомимеют одинаковоедерево решений.В первом случаедерево решенийпросматриваетсядо тех пор, покане будет найденокакое либорешение. Таккак для этихзадач не существуетчастичногоили приближенногорешения, тообычно нельзяиспользоватьдля уменьшенияобъема работыэвристическийподход илиметод ветвейи границ. Обычновсего лишьнесколько путейв дереве ведутк решению, поэтомурешение этихзадач — оченьтрудоемкийпроцесс.
Прирешении жеобобщенногослучая задачи, часто можноиспользоватьчастичныерешения и применитьметод ветвейи границ. Этоне облегчаетпоиск наилучшегорешения задачи, поэтому непоможет получитьточное решениедля частнойзадачи. Например, сложнее найтисамый короткийГамильтоновпуть в сети, чем найтипроизвольныйГамильтоновпуть для тойже сети.
==========222

С другойстороны, этивопросы обычноотносятся кразличнымвходным данным.Обычно вопросо существованииГамильтоновапути возникает, если сеть разрежена, и сложно сказать, существуетли такой путь.Вопрос о кратчайшемГамильтоновомпути возникаетобычно, еслисеть достаточноплотная и существуетмножество такихпутей. В этомслучае легконайти частичныерешения, и методветвей и границможет сильноупроститьрешение задачи.Резюме
Можноиспользоватьдеревья решенийдля моделированияразличныхзадач. Поискнаилучшегорешения задачисоответствуетпри этом поискунаилучшегопути в дереве.К сожалению, деревья решенийдля многихинтересныхзадач имеютогромный размер, поэтому решитьтакие задачиметодом полногоперебора можнотолько дляочень небольшихзадач.
Методветвей и границпозволяетотсекать большуючасть ветвейв некоторыхдеревьях решений, что позволяетполучать точноерешение длязадач гораздобольшего размера.
Тем неменее, для самыхбольших задач, даже применениеметода ветвейи границ неможет помочь.В этом случае, для полученияприблизительногорешения необходимоиспользоватьэвристическийподход дляполученияприблизительныхрешений. Припомощи методовслучайногопоиска и последовательныхприближенийможно найтиприемлемоерешение, дажеесли неизвестно, будет ли ононаилучшимвозможнымрешением задачи.

==========223
Глава9. Сортировка
Сортировка —одна из наиболееактивно изучаемыхтем в компьютерныхалгоритмахпо ряду причин.Во-первых, сортировка —это задача, которая частьвстречаетсяво многихприложениях.Почти любойсписок данныхбудет нестибольше смысла, если его отсортироватькаким либообразом. Частотребуетсясортироватьданные несколькимиразличнымиспособами.
Во вторых, многие алгоритмысортировкиявляются интереснымипримерамипрограммирования.Они демонстрируютважные методы, такие как частичноеупорядочение, рекурсия, слияниесписков и хранениедвоичных деревьевв массиве.
Наконец, сортировкаявляется однойиз немногихзадач с точнымитеоретическимиограничениямипроизводительности.Можно показать, что время выполнениялюбого алгоритмасортировки, который используетсравнения, составляетпорядка O(N * log(N)).Некоторыеалгоритмыдостигаюттеоретическогопредела, тоесть они являютсяоптимальнымив этом смысле.Есть даже ряднесколькоалгоритмов, которые используютдругие методывместо сравнений, которые выполняютсябыстрее, чемза время порядкаO(N * log(N)).Общиесоображения
В этойглаве описанынекоторыеалгоритмысортировки, которые ведутсебя по разномув различныхобстоятельствах.Например, пузырьковаясортировкаопережаетбыструю сортировкупо скоростиработы, еслисортируемыеэлементы ужебыли почтиупорядочены, но работаетмедленнее, еслиэлементы былирасположеныхаотично.
Особенностикаждого алгоритмаописаны в параграфе, в котором онобсуждается.Перед тем какперейти крассмотрениюотдельныхалгоритмов, вначале в этойглаве обсуждаютсявопросы, которыевлияют на всеалгоритмысортировки.Таблицыуказателей
Присортировкеэлементовданных, программаорганизуетиз них некотороеподобие структурыданных. Этотпроцесс можетбыть быстрымили медленнымв зависимостиот типа элементов.Перемещениецелого числана новое положениев массиве можетбыть намногобыстрее, чемперемещениеопределеннойпользователемструктурыданных. Еслиэта структурапредставляетсобой списокданных о сотруднике, содержащийтысячи байтинформации, копированиеодного элементаможет занятьдостаточномного времени.

========225

Дляповышенияпроизводительностипри сортировкебольших объектовможно помещатьключевые поляданных, используемыедля сортировки, в таблицу индексов.В этой таблиценаходятся ключик записям ииндексы элементовдругого массива, в котором инаходятсязаписи данных.Например, предположим, что вы собираетесьотсортироватьсписок записейо сотрудниках, определяемыйследующейструктурой:

TypeEmloyee
IDAs Integer
LastNameAs String
FirstNameAs String
EndType
‘ Выделитьпамять подзаписи.
DimEmloyeeData(1 To 10000)

Чтобыотсортироватьсотрудниковпо идентификационномуномеру, нужносоздать таблицуиндексов, котораясодержит индексыи значения IDvaluesиз записей.Индекс элементапоказывает, какая записьв массивеEmployeeDataсодержитсоответствующиеданные.

TypeIdIndex
IDAs Integer
IndexAs Integer
EndType

‘ Таблицаиндексов.
DimIdIndexData(1 To 10000)

Проинициализируемтаблицу индексовтак, чтобы первыйиндекс указывална первую записьданных, второй —на вторую, ит.д.

Fori = 1 To 10000
IdIndexData(i).ID= EmployeeData(i).ID
IdIndexData(i).Index= i
Nexti

Затем, отсортируемтаблицу индексовпо идентификационномуномеру ID.После этого, поле Indexв каждом элементеIdIndexDataуказывает насоответствующуюзапись данных.Например, перваязапись в отсортированномсписке — этоEmployeeData(IdIndexData(1).Index).На рис. 9.1 показанавзаимосвязьмежду индексоми записью данныхдо, и послесортировки.

=======226

@Рисунок9.1. Сортировкас помощью таблицыиндексов

Длятого, чтобысортироватьданные в разномпорядке, можносоздать несколькоразличныхтаблиц индексови управлятьими по отдельности.В приведенномпримере можнобыло бы создатьеще одну таблицуиндексов, упорядочивающуюсотрудниковпо фамилии.Подобно этомусписки со ссылкамимогут сортироватьсписок различнымиспособами, какпоказано во2 главе. Придобавленииили удалениизаписи необходимообновлятькаждую таблицуиндексов независимо.
Помните, что таблицыиндексов занимаютдополнительнуюпамять. Еслисоздать потаблице индексовдля каждогоиз полей данных, объем занимаемойпамяти болеечем удвоится.Объединениеи сжатие ключей
Иногдаможно хранитьключи спискав комбинированнойили сжатойформе. Например, можно было быобъединить(combine) в программедва поля, соответствующихимени и фамилии, в одни ключ.Это позволилобы упроститьи ускоритьсравнение.Обратите вниманиена различиямежду двумяследующимифрагментамикода, которыесравниваютдве записи осотрудниках:

‘ Используяразные ключи.
Ifemp1.LastName > emp2.LastName Or _
(emp1.LastName= emp2.LastName And _
Andemp1.FirstName > emp2.FirstName) Then
DoSomething

‘ Используяобъединенныйключ.
Ifemp1.CominedName > emp2.CombinedName Then
DoSomething

========227

Такжеиногда можносжимать (comdivss)ключи. Сжатыеключи занимаютменьше места, уменьшая размертаблиц индексов.Это позволяетсортироватьсписки большегоразмера безперерасходапамяти, быстрееперемещатьэлементы всписке, и частотакже ускоряетсравнениеэлементов.
Однииз методовсжатия строк —кодированиеих целыми числамиили даннымидругого числовогоформата. Числовыеданные занимаютменьше места, чем строки исравнение двухчисленныхзначений такжепроисходитнамного быстрее, чем сравнениедвух строк.Конечно, строковыеоперации неприменимыдля строк, представленныхчислами.
Например, предположим, что мы хотимзакодироватьстроки, состоящиеиз заглавныхлатинских букв.Можно считать, что каждыйсимвол — эточисло по основанию27. Необходимоиспользоватьоснование 27, чтобы представить26 букв и еще однуцифру для обозначенияконца слова.Без отметкиконца слова, закодированнаястрока AAшла бы послестроки B, потому что встроке AAдве цифры, а встроке B —одна.
Код пооснованию 27для строки изтрех символовдает формула272 * (первая буква- A + 1) + 27 * (вторая буква- A + 1) + 27 * (третья буква- A + 1). Если в строкеменьше трехсимволов, вместозначения (третьябуква — A + 1) подставляется0. Например, строкаFOXкодируетсятак:

272* (F — A + 1) + 27 * (O — A + 1) + (X — A+1) = 4803

СтрокаNOкодируетсяследующимобразом:

272* (N — A + 1) + 27 * (O — A + 1) + (0) =10.611

Заметим, что 10.611 больше4803, посколькуNO> FOX.
Такимже образомможно закодироватьстроки из 6 заглавныхбукв в видечисла в форматеlongи строки из 10букв — как числов формате double.Две следующиепроцедурыконвертируютстроки в числав формате doubleи обратно:

ConstSTRING_BASE = 27
ConstASC_A = 65 ‘ ASCII коддля символа«A».
‘ Преобразованиестроки с числов формате double.

‘ full_len —полная длина, которую должнаиметь строка.
‘ Нужна, если строкаслишком короткая(например «AX» —
‘ этострока из трехсимволов).
FunctionStringToDbl (txt As String, full_len AsInteger) As Double
Dimstrlen As Integer
Dimi As Integer
Dimvalue As Double
Dimch As String * 1

strlen= Len(txt)
Ifstrlen > full_len Then strlen = full_len

value= 0#
Fori = 1 To strlen
ch= Mid$(txt, i, 1)
value= value * STRING_BASE + Asc(ch) — ASC_A + 1
Nexti

Fori = strlen + 1 To full_len
value= value * STRING_BASE
Nexti
EndFunction

‘ Обратноедекодированиестроки из форматаdouble.
FunctionDblToString (ByVal value As Double) As String
Dimstrlen As Integer
Dimi As Integer
Dimtxt As String
DimPower As Integer
Dimch As Integer
Dimnew_value As Double
ит.д.>    продолжение
--PAGE_BREAK--
txt= ""
DoWhile value > 0
new_value= Int(value / STRING_BASE)
ch= value — new_value * STRING_BASE
Ifch 0 Then txt = Chr$(ch + ASC_A — 1) + txt
value= new_value
Loop

DblToString= txt
EndFunction

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

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

========229

@Таблица9.1. Время сортировки2000 строк с использованиемразличныхкодировок всекундах

Можнотакже кодироватьстроки, состоящиене только иззаглавных букв.Строку из заглавныхбукв и цифрможно закодироватьпо основанию37 вместо 27. Кодбуквы A будетравен 1, B — 2, …, Z— 26, код 0 будет27, …, и 9 — 36. СтрокаAH7будет кодироватьсякак 372 * 1 + 37 * 8 + 35 = 1700.
Конечно, при использованиибольшего основания, длина строки, которую можнозакодироватьчислом типаinteger,longили doubleбудет соответственнокороче. Приоснованииравном 37, можнозакодироватьстроку из 2 символовв числе форматаinteger, из 5 символовв числе форматаlong, и 10 символов вчисле форматаdouble.Примерыпрограмм
Чтобыоблегчитьсравнениеразличныхалгоритмовсортировки, программа Sortдемонстрируетбольшинствоалгоритмов, описанных вэтой главе.Сортировкапозволяетзадать числосортируемыхэлементов, ихмаксимальноезначение, ипорядок расположенияэлементов — прямой, обратныйили расположениев случайномпорядке. Программасоздает списокслучайнорасположенныхчисел в форматеlongи сортируетего, используявыбранныйалгоритм. Вначалесортируйтекороткие списки, пока не определите, насколькобыстро вашкомпьютер можетвыполнятьоперации сортировки.Это особенноважно для медленныхалгоритмовсортировкивставкой, сортировкивставкой сиспользованиемсвязного списка, сортировкивыбором, ипузырьковойсортировки.
Некоторыеалгоритмыперемещаютбольшие блокипамяти. Например, алгоритм сортировкивставкой перемещаетэлементы спискадля того, чтобыможно быловставить новыйэлемент в серединусписка. Дляперемещенияэлементовпрограмме, написаннойна Visual Basic, приходитсяиспользоватьцикл For.Следующий кодпоказывает, как сортировкавставкой перемещаетэлементы сList(j)до List(max_sorted)для того, чтобыосвободитьместо под новыйэлемент в позицииList(j):

Fork = max_sorted To j Step -1
List(k+ 1) = List(k)
Nextk
List(j)= next_num

==========230

Интерфейсприкладногопрограммированиясистемы Windowsвключает двефункции, которыепозволяютнамного быстреевыполнятьперемещениеблоков памяти.Программы, скомпилированные16 битной версиейкомпилятораVisual Basic 4, могут использоватьфункцию hmemcopy.Программы, скомпилированные32 битнымикомпиляторамиVisual Basic 4 и5, могут использоватьфункцию RtlMoveMemory.Обе функциипринимают вкачестве параметровконечный иисходный адресаи число байт, которое должнобыть скопировано.Следующий кодпоказывает, как объявлятьэти функциив модуле .BAS:

#ifWin16 Then
DeclareSub MemCopy Lib «Kernel» Alias _
«hmemcpy»(dest As Any, src As Any, _
ByValnumbytes As Long)
#Else
DeclareSub MemCopy Lib «Kernel32» Alias _
«RtlMoveMemory»(dest As Any, src As Any, _
ByValnumbytes As Long)
#EndIf

Следующийфрагмент кодапоказывает, как сортировкавставкой можетиспользоватьэти функциидля копированияблоков памяти.Этот код выполняетте же действия, что и цикл For, приведенныйвыше, но делаетэто намногобыстрее:

Ifmax_sorted >= j Then _
MemCopyList(j + 1), List(j), _
Len(next_num)* (max_sorted — j + 1)
List(j)= next_num

ПрограммаFastSortаналогичнапрограмме Sort, но она используетфункцию MemCopyдля ускоренияработы некоторыхалгоритмов.В программеFastSortалгоритмы, использующиефункцию MemCopy, выделены синимцветом.Сортировкавыбором
Сортировкавыбором(selectionsort) — простойалгоритм сосложностьпорядка O(N2).Идея состоитв поиске наименьшегоэлемента всписке, которыйзатем меняетсяместами с элементомна вершинесписка. Затемнаходитсянаименьшийэлемент изоставшихся, и меняетсяместами совторым элементом.Процесс продолжаетсядо тех пор, покавсе элементыне займут своеконечное положение.

PublicSub Selectionsort(List() As Long, min As Long, max As Long)
Dimi As Long
Dimj As Long
Dimbest_value As Long
Dimbest_j As Long

Fori = min To max — 1
‘Найти наименьшийэлемент изоставшихся.
best_value= List(i)
best_j= i
Forj = i + 1 To max
IfList(j)
best_value= List(j)
best_j= j
EndIf
Nextj

‘Поместитьэлемент наместо.
List(best_j)= List(i)
List(i)= best_value
Nexti
EndSub

========231

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

Iflist(j)
best_value= list(j)
best_j= j
EndIf

Еслипервоначальносписок отсортированв обратномпорядке, условиеlist(j)
Это несамый быстрыйалгоритм изчисла описанныхв главе, но ончрезвычайнопрост. Это нетолько облегчаетего разработкуи отладку, нои делает сортировкувыбором достаточнобыстрой длянебольшихзадач. Многиедругие алгоритмынастолькосложны, что онисортируют оченьмаленькиесписки медленнее.Рандомизация
В некоторыхпрограммахтребуетсявыполнениеоперации, обратнойсортировке.Получив списокэлементов, программадолжна расположитьих в случайномпорядке. Рандомизацию(unsorting) списканесложно выполнить, используяалгоритм, похожийна сортировкувыбором.
Длякаждого положенияв списке, алгоритмслучайнымобразом выбираетэлемент, которыйдолжен егозанять из тех, которые ещене были помещенына свое место.Затем этотэлемент меняетсяместами с элементом, который, находитсяна этой позиции.

PublicSub Unsort(List() As Long, min As Long, maxAs Long)
Dimi As Long
DimPos As Long
Dimtmp As Long

Fori — min To max — 1
pos= Int((max — i + 1) * Rnd + i)
tmp= List(pos)
List(pos)= List(i)
List(i)= tmp
Nexti
EndSub

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

Т.к.алгоритм заполняеткаждую позициютолько одинраз, его сложностьпорядка O(N).
Несложнопоказать, чтовероятностьтого, что элементокажется накакой либопозиции, равна1/N. Посколькуэлемент можетоказаться влюбом положениис равной вероятностью, этот алгоритмдействительноприводит кслучайномуразмещениюэлементов.
Результатзависит оттого, насколькохорошим являетсягенераторслучайныхчисел. ФункцияRndв Visual Basicдает приемлемыйрезультат длябольшинстваслучаев. Следуетубедиться, чтопрограммаиспользуетоператор Randomizeдля инициализациифункции Rnd, иначе при каждомзапуске программыфункция Rndбудет выдаватьодну и ту жепоследовательность«случайных»значений.
Заметим, что для алгоритмане важен первоначальныйпорядок расположенияэлементов. Есливам необходимонеоднократнорандомизироватьсписок элементов, нет необходимостиего предварительносортировать.
ПрограммаUnsortпоказываетиспользованиеэтого алгоритмадля рандомизацииотсортированногосписка. Введитечисло элементов, которые выхотите рандомизировать, и нажмите кнопкуGo (Начать).Программапоказываетисходныйотсортированныйсписок чисели результатрандомизации.Сортировкавставкой
Сортировкавставкой(insertionsort) — ещеодин алгоритмсо сложностьюпорядка O(N2).Идея состоитв том, чтобысоздать новыйсортированныйсписок, просматриваяпоочередновсе элементыв исходномсписке. Приэтом, выбираяочереднойэлемент, алгоритмпросматриваетрастущийотсортированныйсписок, находиттребуемоеположениеэлемента в нем, и помещаетэлемент на своеместо в новыйсписок.

PublicSub Insertionsort(List() As Long, min As Long, max As Long)
Dimi As Long
Dimj As Long
Dimk As Long
Dimmax_sorted As Long
Dimnext_num As Long

max_sorted= min -1
Fori = min To max
‘Это вставляемоечисло.
Next_num= List(i)

‘Поиск его позициив списке.
Forj = min To max_sorted
IfList(j) >= next_num Then Exit For
Nextj

‘Переместитьбольшие элементывниз, чтобы
‘освободитьместо для новогочисла.
Fork = max_sorted To j Step -1
List(k+ 1) = List(k)
Nextk

‘Поместить новыйэлемент.
List(j)= next_num

‘Увеличитьсчетчик отсортированныхэлементов.
max_sorted= max_sorted + 1
Nexti
EndSub

=======233

Можетоказаться, чтодля каждогоиз элементовв исходномсписке, алгоритмупридется проверятьвсе уже отсортированныеэлементы. Этопроисходит, например, еслив исходномсписке элементыбыли уже отсортированы.В этом случае, алгоритм помещаеткаждый новыйэлемент в конецрастущегоотсортированногосписка.
Полноечисло шагов, которые потребуетсявыполнить, составляет1 + 2 + 3 + … + (N — 1), то естьO(N2). Это не слишкомэффективно, если сравнитьс теоретическимпределом O(N *log(N)) для алгоритмовна основе операцийсравнения.Фактически, этот алгоритмне слишкомбыстр даже всравнении сдругими алгоритмамипорядка O(N2), такими каксортировкавыбором.
Достаточномного времениалгоритм сортировкивставкой тратитна перемещениеэлементов длятого, чтобывставить новыйэлемент в серединуотсортированногосписка. Использованиедля этого функцииAPI MemCopyувеличиваетскорость работыалгоритма почтивдвое.
Достаточномного временитратится и напоиск правильногоположения длянового элемента.В 10 главе описанонесколькоалгоритмовпоиска в отсортированныхсписках. Применениеалгоритмаинтерполяционногопоиска намногоускоряет выполнениеалгоритмасортировкивставкой.Интерполяционныйпоиск подробноописываетсяв 10 главе, поэтомумы не будемсейчас на немостанавливаться.
ПрограммаFastSortиспользуетоба этих методадля улучшенияпроизводительностисортировкивставкой. Сиспользованиемфункции MemCopyи интерполяционногопоиска, этаверсия алгоритмаболее чем в 15раз быстрее, чем исходная.Вставкав связных списках
Можноиспользоватьвариант сортировкивставкой дляупорядоченияэлементов нев массиве, а всвязном списке.Этот алгоритмищет требуемоеположениеэлемента врастущем связномсписке, и затемпомещает тудановый элемент, используяоперации работысо связнымисписками.

=========234

PublicSub LinkInsertionSort(ListTop As ListCell)
Dimnew_top As New ListCell
Dimold_top As ListCell
Dimcell As ListCell
Dimafter_me As ListCell
Dimnxt As ListCell

Setold_top = ListTop.NextCell
DoWhile Not (old_top Is Nothing)
Setcell = old_top
Setold_top = old_top.NextCell

‘Найти, куданеобходимопоместитьэлемент.
Setafter_me = new_top
Do
Setnxt = after_me.NextCell
Ifnxt Is Nothing Then Exit Do
Ifnxt.Value >= cell.Value Then Exit Do
Setafter_me = nxt
Loop

‘Вставить элементпосле позицииafter_me.
Setafter_me.NextCll = cell
Setcell.NextCell = nx
Loop
SetListTop.NextCell = new_top.NextCell
EndSub

Т.к. этоталгоритм перебираетвсе элементы, может потребоватьсясравнениекаждого элементасо всеми элементамив отсортированномсписке. В этомнаихудшемслучае вычислительнаясложностьалгоритмапорядка O(N2).
Наилучшийслучай дляэтого алгоритмадостигается, когда исходныйсписок первоначальноотсортированв обратномпорядке. Приэтом каждыйпоследующийэлемент меньше, чем предыдущий, поэтому алгоритмпомещает егов начало отсортированногосписка. Приэтом требуетсявыполнитьтолько однуоперацию сравненияэлементов, ив наилучшемслучае времявыполненияалгоритма будетпорядка O(N).
В усредненномслучае, алгоритмупридется провестипоиск примернопо половинеотсортированногосписка длятого, чтобынайти местоположениеэлемента. Приэтом алгоритмвыполняетсяпримерно за1 + 1 + 2 + 2 + … + N/2, или порядкаO(N2) шагов.
Улучшеннаяпроцедурасортировкивставкой, использующаяинтерполяционныйпоиск и функциюMemCopy, работает намногобыстрее, чемверсия со связнымсписком, поэтомупоследнююпроцедуру лучшеиспользовать, если программауже хранитэлементы всвязном списке.
Преимуществоиспользованиясвязных списковдля вставкив том, что приэтом перемещаютсятолько указатели, а не сами записиданных. Передачауказателейможет бытьбыстрее, чемкопированиезаписей целиком, если элементыпредставляютсобой большиеструктурыданных.

=======235
Пузырьковаясортировка
Пузырьковаясортировка(bubblesort) — этоалгоритм, предназначенныйдля сортировкисписков, которыеуже находятсяв почти упорядоченномсостоянии. Еслив начале процедурысписок полностьюотсортирован, алгоритм выполняетсяочень быстроза время порядкаO(N). Если частьэлементовнаходятся нена своих местах, алгоритм выполняетсямедленнее. Еслипервоначальноэлементы расположеныв случайномпорядке, алгоритмвыполняетсяза время порядкаO(N2). Поэтомуперед применениемпузырьковойсортировкиважно убедиться, что элементыв основномрасположеныпо порядку.
Припузырьковойсортировкесписок просматриваетсядо тех пор, покане найдутсядва соседнихэлемента, расположенныхне по порядку.Тогда они меняютсяместами, и процедурапродолжаетсядальше. Алгоритмповторяет этотпроцесс до техпор, пока всеэлементы незаймут своиместа.
На рис.9.2 показано, какалгоритм вначалеобнаруживает, что элементы6 и 3 расположеныне по порядку, и поэтому меняетих местами. Вовремя следующегопрохода, меняютсяместами элементы5 и 3, в следующем —4 и 3. После ещеодного проходаалгоритмобнаруживает, что все элементырасположеныпо порядку, изавершаетработу.
Можнопроследитьза перемещениямиэлемента, которыйпервоначальнобыл расположенниже, чем послесортировки, например элемента3 на рис. 9.2. Во времякаждого проходаэлемент перемещаетсяна одну позициюближе к своемуконечномуположению. Ондвижется квершине спискаподобно пузырькугаза, которыйвсплывает кповерхностив стакане воды.Этот эффекти дал названиеалгоритмупузырьковойсортировки.
Можновнести в алгоритмнесколькоулучшений.Во первых, еслиэлемент расположенв списке выше, чем должнобыть, вы увидитекартину, отличнуюот той, котораяприведена нарис. 9.2. На рис. 9.3показано, чтоалгоритм вначалеобнаруживает, что элементы6 и 3 расположеныв неправильномпорядке, и меняетих местами.Затем алгоритмпродолжаетпросматриватьмассив и замечает, что теперьнеправильнорасположеныэлементы 6 и 4, и также меняетих местами.Затем меняютсяместами элементы6 и 5, и элемент6 занимает своеместо.

@Рис.9.2. «Всплывание»элемента

========236

@Рис.9.3. «Погружение»элемента

Припросмотремассива сверхувниз, элементы, которые перемещаютсявверх, сдвигаютсявсего на однупозицию. Те жеэлементы, которыеперемещаютсявниз, сдвигаютсяна несколькопозиций за одинпроход. Этотфакт можноиспользоватьдля ускоренияработы алгоритмапузырьковойсортировки.Если чередоватьпросмотр массивасверху внизи снизу вверх, то перемещениеэлементов впрямом и обратномнаправленияхбудет одинаковобыстрым.
Во времяпроходов сверхувниз, наибольшийэлемент спискаперемещаетсяна место, а вовремя проходовснизу вверх —наименьший.Если M элементовсписка расположеныне на своихместах, алгоритмупотребуетсяне более M проходовдля того, чтобырасположитьэлементы попорядку. Еслив списке N элементов, алгоритмупотребуетсяN шагов для каждогопрохода. Такимобразом, полноевремя выполнениядля этого алгоритмабудет порядкаO(M * N).
Еслипервоначальносписок организованслучайнымобразом, большаячасть элементовбудет находитьсяне на своихместах. В примере, приведенномна рис. 9.3, элемент6 трижды меняетсяместами с соседнимиэлементами.Вместо выполнениятрех отдельныхперестановок, можно сохранитьзначение 6 вовременнойпеременнойдо тех пор, покане будет найденоконечное положениеэлемента. Этоможет сэкономитьбольшое числошагов алгоритма, если элементыперемещаютсяна большиерасстояниявнутри массива.
Последнееулучшение —ограничениепроходов массива.После просмотрамассива, последниепереставленныеэлементы обозначаютчасть массива, которая содержитнеупорядоченныеэлементы. Припроходе сверхувниз, например, наибольшийэлемент перемещаетсяв конечноеположение.Поскольку нетбольших элементов, которые нужнобыло бы поместитьза ним, то можноначать очереднойпроход снизувверх с этойточки и на нейже заканчиватьследующиепроходы сверхувниз.

========237
    продолжение
--PAGE_BREAK--
Такимже образом, после проходаснизу вверх, можно сдвинутьпозицию, с которойначнется очереднойпроход сверхувниз, и будутзаканчиватьсяпоследующиепроходы снизувверх.
Реализацияалгоритмапузырьковойсортировкина языке VisualBasic используетпеременныеminи maxдля обозначенияпервого и последнегоэлементовсписка, которыенаходятся нена своих местах.По мере того, как алгоритмаповторяетпроходы посписку, этипеременныеобновляются, указывая положениепоследнейперестановки.

PublicSub Bubblesort(List() As Long, ByVal min As Long, ByVal max As Long)
Dimlast_swap As Long
Dimi As Long
Dimj As Long
Dimtmp As Long

‘Повторять дозавершения.
DoWhile min
‘«Всплывание».
last_swap= min — 1
‘То естьFor i = min + 1 To max.
i= min + 1
DoWhile i
‘Найти«пузырек».
IfList(i — 1) > List(i) Then
‘Найти, куда егопоместить.
tmp= List(i — 1)
j= i
Do
List(j- 1) = List(j)
j= j + 1
Ifj > max Then Exit Do
LoopWhile List(j)
List(j- 1) = tmp
last_swap= j — 1
i= j + 1
Else
i= i + 1
EndIf
Loop
‘Обновить переменнуюmax.
max= last_swap — 1

‘«Погружение».
last_swap= max + 1
‘То естьFor i = max -1 To min Step -1
i= max — 1
DoWhile i >= min
‘Найти«пузырек».
IfList(i + 1)
‘Найти, куда егопоместить.
tmp= List(i + 1)
j= i
Do
List(j+ 1) = List(j)
j= j — 1
Ifj
LoopWhile List(j) > tmp
List(j+ 1) = tmp
last_swap= j + 1
i= j — 1
Else
i= i — 1
EndIf
Loop
‘Обновить переменнуюmin.
Min= last_swap + 1
Loop
EndSub

==========238

Длятого чтобыпротестироватьалгоритм пузырьковойсортировкипри помощипрограммы Sort, поставьтегалочку в полеSorted (Отсортированные)в области InitialOrdering (Первоначальныйпорядок). Введитечисло элементовв поле #Unsorted(Число несортированных).После нажатияна кнопку Go(Начать), программасоздает и сортируетсписок, а затемпереставляетслучайно выбранныепары элементов.Например, есливы введетечисло 10 в поле#Unsorted, программапереставит5 пар чисел, тоесть 10 элементовокажутся нена своих местах.
Длявторого вариантапервоначальногоалгоритма, программасохраняетэлемент вовременнойпеременнойпри перемещениина несколькошагов. Этотпроисходитеще быстрее, если использоватьфункцию API MemCopy.Алгоритм пузырьковойсортировкив программеFastSort, используяфункцию MemCopy, сортируетэлементы в 50или 75 раз быстрее, чем первоначальнаяверсия, реализованнаяв программеSort.
В табл.9.2 приведеновремя выполненияпузырьковойсортировки2000 элементовна компьютерес процессоромPentium с тактовойчастотой 90 МГцв зависимостиот степенипервоначальнойупорядоченностисписка. Из таблицывидно, что алгоритмпузырьковойсортировкиобеспечиваетхорошую производительность, только еслисписок с самогоначала почтиотсортирован.Алгоритм быстройсортировки, который описываетсядалее в этойглаве, способенотсортироватьтот же списокиз 2000 элементовпримерно за0,12 сек, независимоот первоначальногопорядка расположенияэлементов всписке. Пузырьковаясортировкаможет превзойтиэтот результат, только еслипримерно 97 процентовсписка былоупорядоченодо начала сортировки.

=====239

@Таблица9.2. Время пузырьковойсортировки2.000 элементов

Несмотряна то, что пузырьковаясортировкамедленнее, чеммногие другиеалгоритмы, унее есть своиприменения.Пузырьковаясортировкачасто даетнаилучшиерезультаты, если списокизначальноуже почти упорядочен.Если программауправляетсписком, которыйсортируетсяпри создании, а затем к немудобавляютсяновые элементы, пузырьковаясортировкаможет бытьлучшим выбором.Быстраясортировка
Быстраясортировка(quicksort) — рекурсивныйалгоритм, которыйиспользуетподход «разделяйи властвуй».Если сортируемыйсписок больше, чем минимальныйзаданный размер, процедурабыстрой сортировкиразбивает егона два подсписка, а затем рекурсивновызывает себядля сортировкидвух подсписков.
Перваяверсия алгоритмабыстрой сортировки, обсуждаемаяздесь, достаточнопроста. Еслиалгоритм вызываетсядля подсписка, содержащегоне более одногоэлемента, топодсписок ужеотсортирован, и подпрограммазавершаетработу.
Иначе, процедуравыбирает какой либоэлемент изсписка и используетего для разбиениясписка на дваподсписка. Онапомещает элементы, которые меньше, чем выбранныйэлементы впервый подсписок, а остальные —во второй, изатем рекурсивновызывает себядля сортировкидвух подсписков.

PublicSub QuickSort(List() As Long, ByVal min as Integer, _
ByValmax As Integer)
Dimmed_value As Long
Dimhi As Integer
Dimlo As Integer

‘Если осталосьменее 1 элемента, подсписокотсортирован.
Ifmin >= max Then Exit Sub

‘Выбрать значениедля делениясписка.
med_value= list(min)
lo= min
hi= max
Do
Просмотр отhi до значения
DoWhile list(hi) >= med_value
hi= hi — 1
Ifhi
Loop
Ifhi
list(lo)= med_value
ExitDo
EndIf
‘Поменять местамизначения lo иhi.
list(lo)= list(hi)

‘Просмотр отlo до значения>= med_value.
lo= lo + 1
DoWhile list(lo)
lo= lo + 1
Iflo >= hi Then Exit Do
Loop
Iflo >= hi Then
lo= hi
list(hi)= med_value
ExitDo
EndIf
‘Поменять местамизначения lo иhi.
list(hi)= list(lo)
Loop

‘Рекурсивнаясортировкадвух подлистов.
QuickSortlist(), min, lo — 1
QuickSortlist(), lo + 1, max
EndSub

=========240

Естьнескольковажных моментовв этой версииалгоритма, которые стоитупомянуть.Во первых, значение med_valueдля делениясписка не входитни в один подсписок.Это означает, что в двух подспискахсодержитсяна одни элементменьше, чем висходном списке.Т.к. число рассматриваемыхэлементовуменьшается, то в конечномитоге алгоритмзавершит работу.
Этаверсия алгоритмаиспользуетв качестверазделителяпервый элементв списке. В идеале, это значениедолжно былобы находитьсягде то в серединесписка, такчтобы два подспискабыли примерноравного размера.Тем не менее, если элементыпервоначальнопочти отсортированы, то первый элемент —наименьшийв списке. Приэтом алгоритмне поместитни одного элементав первый подсписок, и все элементыво второй.Последовательностьдействий алгоритмабудет примернотакой, как показанона рис. 9.4.
В этомслучае каждыйвызов подпрограммытребует порядкаO(N) шагов дляперемещениявсех элементовво второй подсписок.Т.к. алгоритмрекурсивновызывает себяN — 1 раз, время еговыполнениябудет порядкаO(N2), что не лучше, чем у ранеерассмотренныхалгоритмов.Ситуацию ещеболее ухудшаетто, что уровеньвложенностирекурсии алгоритмаN — 1. Для большихсписков огромнаяглубина рекурсииприведет кпереполнениюстека и сбоюв работе программы.

=========242

@Рис.9.4. Быстрая сортировкаупорядоченногосписка

Существуетмного стратегийвыбора разделительногоэлемента. Можноиспользоватьэлемент изсередины списка.Это может оказатьсянеплохим выбором, тем не менее, может оказатьсяи так, что этоокажется наименьшийили наибольшийэлемент списка.При этом одинподсписок будетнамного больше, чем другой, чтоприведет кснижениюпроизводительностидо порядкаO(N2) и глубокомууровню рекурсии.
Другаястратегия можетзаключатьсяв том, чтобыпросмотретьвесь список, вычислитьсреднее арифметическоевсех значений, и использоватьего в качестверазделительногозначения. Этотподход будетдавать неплохиерезультаты, но потребуетдополнительныхусилий. Дополнительныйпроход со сложностьюпорядка O(N) неизменит теоретическоевремя выполненияалгоритма, носнизит общуюпроизводительность.
Третьястратегия —выбрать среднийиз элементовв начале, концеи серединесписка. Преимуществоэтого подходав быстроте, потому чтопотребуетсявыбрать всеготри элемента.При этом гарантируется, что этот элементне являетсянаибольшимили наименьшимв списке, и вероятноокажется где тов серединесписка.
И, наконец, последняястратегия, которая используетсяв программеSort, заключаетсяв случайномвыборе элементаиз списка. Возможно, это будет неплохимвыбором. Дажеесли это нетак, возможнона следующемшаге алгоритм, возможно, сделаетлучший выбор.Вероятностьпостоянноговыпадениянаихудшегослучая оченьмала.
Интересно, что этот методпревращаетситуацию «небольшаявероятностьтого, что всегдабудет плохаяпроизводительность»в ситуацию«всегда небольшаявероятностьплохой производительности».Это довольнозапутанноеутверждениеобъясняетсяв следующихабзацах.
Прииспользованиидругих методоввыбора точкираздела, существуетнебольшаявероятностьтого, что приопределеннойорганизациисписка времясортировкибудет порядкаO(N2), Хотя маловероятно, что подобнаяорганизациясписка в началесортировкивстретитсяна самом деле, тем не менее, время выполненияпри этом будетопределеннопорядка O(N2), неважно почему.Это то, что можноназвать «небольшойвероятностьютого, что всегдабудет плохаяпроизводительность».

===========242

Прислучайномвыборе точкираздела первоначальноерасположениеэлементов невлияет напроизводительностьалгоритма.Существуетнебольшаявероятностьнеудачноговыбора элемента, но вероятностьтого, что этобудет происходитьпостоянно, чрезвычайномала. Это можнообозначитькак «всегданебольшаявероятностьплохой производительности».Независимоот первоначальнойорганизациисписка, оченьмаловероятно, что производительностьалгоритма будетпорядка O(N2).
Тем неменее, все ещеостается ситуация, которая можетвызвать проблемыпри использованиилюбого из этихметодов. Еслив списке оченьмало различныхзначений всписке, алгоритмзаносит множествоодинаковыхзначений вподсписок прикаждом вызове.Например, есликаждый элементв списке имеетзначение 1, последовательностьвыполнениябудет такой, как показанона рис. 9.5. Этоприводит кбольшому уровнювложенностирекурсии и даетпроизводительностьпорядка O(N2).
Похожееповедениепроисходиттакже при наличиибольшого числаповторяющихсязначений. Еслисписок состоитиз 10.000 элементовсо значениямиот 1 до 10, алгоритмдовольно быстроразделит списокна подсписки, каждый из которыхсодержит толькоодно значение.
Наиболеепростой выход —игнорироватьэту проблему.Если вы знаете, что данные неимеют такогораспределения, то проблемынет. Если данныеимеют небольшойдиапазон значений, то вам стоитрассмотретьдругой алгоритмсортировки.Описываемыедалее в этойглаве алгоритмысортировкиподсчетом иблочной сортировкиочень быстросортируютсписки, данныхв которых находятсяв узком диапазоне.
Можновнести еще однонебольшоеулучшение валгоритм быстройсортировки.Подобно многихдругим болеесложным алгоритмам, описанным далеев этой главе, быстрая сортировка —не самый лучшийвыбор для сортировкинебольшихсписков. Благодарясвоей простоте, сортировкавыбором быстреепри сортировкепримерно десятказаписей.

@Рис.9.5. Быстрая сортировкасписка из единиц

==========243

@Таблица9.3. Время быстройсортировки20.000 элементов

Можноулучшитьпроизводительностьбыстрой сортировки, если прекратитьрекурсию дотого, как подспискиуменьшатсядо нуля, и использоватьдля завершенияработы сортировкувыбором. В табл.9.3 приведеновремя, котороезанимает выполнениебыстрой сортировки20.000 элементовна компьютерес процессоромPentium с тактовойчастотой 90 МГц, если останавливатьсортировкупри достиженииподспискамиопределенногоразмера. В этомтесте оптимальноезначение этогопараметра былоравно 15.
Следующийкод демонстрируетдоработанныйалгоритм:

PublicSub QuickSort*List() As Long, ByVal min As Long, ByVal max As Long)
Dimmed_value As Long
Dimhi As Long
Dimlo As Long
Dimi As Long

‘Если в спискебольше, чемCutOff элементов,
‘завершить егосортировкупроцедуройSelectionSort.
Ifmax — min
SelectionSortList(), min, max
ExitSub
EndIf

‘Выбрать разделяющеезначение.
i= Int((max — min + 1) * Rnd + min)
med_value= List(i)

‘Переместитьего вперед.
List(i)= List(min)

lo= min
hi= max
Do
‘Просмотр сверхувниз от hi дозначения
DoWhile List(hi) >= med_value
hi= hi — 1
Ifhi
Loop
Ifhi
List(lo)= med_value
ExitDo
EndIf

‘Поменять местамизначения lo иhi.
List(lo)= List(hi)

‘Просмотр снизувверх от lo дозначения >=med_value.
lo= lo + 1
DoWhile List(lo)
lo= lo + 1
Iflo >= hi Then Exit Do
Loop
Iflo >= hi Then
lo= hi
List(hi)= med_value
ExitDo
EndIf

‘Поменять местамизначения lo иhi.
List(hi)= List(lo)
Loop

‘Сортироватьдва подсписка.
QuickSortList(), min, lo — 1
QuickSortList(), lo + 1, max
EndSub

=======244

Многиепрограммистывыбирают алгоритмбыстрой сортировки, т.к. он дает хорошуюпроизводительностьв большинствеобстоятельств.Сортировкаслиянием
Как ибыстрая сортировка,сортировкаслиянием(mergesort) — эторекурсивныйалгоритм. Онтакже разделяетсписок на дваподсписка, ирекурсивносортируетподсписки.
Сортировкаслиянием делитсписок пополам, формируя дваподспискаодинаковогоразмера. Затемподспискирекурсивносортируются, и отсортированныеподспискисливаются, образуя полностьюотсортированныйсписок.
Хотяэтап слияниялегко понять, это наиболееинтереснаячасть алгоритма.Подспискисливаются вовременныймассив, и результаткопируетсяв первоначальныйсписок. Созданиевременногомассива можетбыть недостатком, особенно еслиразмер элементоввелик. Еслиразмер временногоразмера оченьбольшой, онможет приводитьк обращениюк файлу подкачкии значительноснижать производительность.Работа с временныммассивом такжеприводит ктому, что большаячасть времениуходит на копированиеэлементов междумассивами.
Также, как и в случаес быстройсортировкой, можно ускоритьвыполнениесортировкислиянием, остановиврекурсию, когдаподспискидостигаютопределенногоминимальногоразмера. Затемможно использоватьсортировкувыбором длязавершенияработы.

=========245

PublicSub Mergesort(List() As Long, Scratch() As Long, _
ByValmin As Long, ByVal max As Long)
Dimmiddle As Long
Dimi1 As Long
Dimi2 As Long
Dimi3 As Long

‘Если в спискебольше, чемCutOff элементов,
‘завершить егосортировкупроцедуройSelectionSort.
Ifmax — min
SelectionsortList(), min, max
ExitSub
EndIf

‘Рекурсивнаясортировкаподсписков.
middle= max \ 2 + min \ 2
MergesortList(), Scratch(), min, middle
MergesortList(), Scratch(), middle + 1, max

‘Слить отсортированныесписки.
i1= min ‘ Индекс списка1.
i2= middle + 1 ‘ Индекссписка 2.
i3= min ‘ Индексобъединенногосписка.
DoWhile i1
IfList(i1)
Scratch(i3)= List(i1)
i1= i1 + 1
Else
Scratch(i3)= List(i2)
i2= i2 + 1
endIf
i3= i3 + 1
Loop

‘Очистка непустогосписка.
DoWhile i1
Scratch(i3)= List(i1)
i1= i1 + 1
i3= i3 + 1
Loop
DoWhile i2
Scratch(i3)= List(i2)
i2= i2 + 1
i3= i3 + 1
Loop

‘Поместитьотсортированныйсписок на местоисходного.
Fori3 = min To max
List(i3)= Scratch(i3)
Nexti3
EndSub

========246

Сортировкаслиянием тратитмного временина копированиевременногомассива наместо первоначального.ПрограммаFastSortиспользуетфункцию API MemCopy, чтобы немногоускорить этуоперацию.
Дажес использованиемфункции MemCopy, сортировкаслиянием немногомедленнее, чембыстрая сортировка.В нашем тестена компьютерес процессоромPentium с тактовойчастотой 90 МГц, сортировкаслиянием потребовала2,95 сек для упорядочения30.000 элементовсо значениямив диапазонеот 1 до 10.000. Быстраясортировкапотребовалавсего 2,44 сек.
Преимуществосортировкислиянием в том, что время еевыполненияостается одинаковымнезависимоот различныхраспределенийи начальногорасположенияданных. Быстраяже сортировкадает производительностьпорядка O(N2) идостигаетглубокогоуровня вложенностирекурсии, еслисписок содержитмного одинаковыхзначений. Еслисписок большой, быстрая сортировкаможет переполнитьстек и привестик аварийномузавершениюработы программы.Сортировкаслиянием никогдане достигаетслишком глубокогоуровня вложенностирекурсии, т.к.всегда делитсписок на равныечасти. Для спискаиз N элементов, глубина вложенностирекурсии длясортировкислиянием составляетвсего лишьlog(N).
В другомтесте, в которомиспользовались30.000 элементовсо значениямиот 1 до 100, сортировкаслиянием потребоваластолько жевремени, сколькои для элементовсо значениямиот 1 до 10.000 — 2,95 секунд.Быстрая сортировказаняла 15,82 секунды.Если значениялежали между1 и 50, сортировкаслиянием потребовала2,95 секунд, тогдакак быстраясортировка —138,52 секунды.Пирамидальнаясортировка
Пирамидальнаясортировка(heapsort) используетспециальнуюструктуру, называемуюпирамидой(heap), для организацииэлементов всписке. Пирамидыинтересны самипо себе и полезныпри реализацииприоритетныхочередей.
В началеэтой главыописываютсяпирамиды, иобъясняется, как вы можетереализоватьпирамиды наязыке VisualBasic. Затемпоказано, какиспользоватьпирамиду дляпостроенияэффективнойприоритетнойочереди. Располагаясредствамидля управленияпирамидамии приоритетнымиочередями, легко реализоватьалгоритмпирамидальнойсортировки.Пирамиды
Пирамида(heap) — этополное двоичноедерево, в которомкаждый узелне меньше, чемоба его потомка.Это ничего неговорит о взаимосвязимежду потомками.Они должны бытьменьше родителя, но любой из нихможет бытьбольше, чемдругой. На рис.9.6 показананебольшаяпирамида.
Посколькукаждый узелне меньше, чемдва нижележащихузла, кореньдерева — всегданаибольшийэлемент в пирамиде.Это делаетпирамиды удобнойструктуройданных дляреализацииприоритетныхочередей. Есливам нужен элементочереди с самымвысоким приоритетом, он всегда находитсяна вершинепирамиды.
    продолжение
--PAGE_BREAK--
=========247

Рис.9.6. Пирамида

Посколькупирамида являетсяполным двоичнымдеревом, выможете использоватьметоды, изложенныев 6 главе, длясохраненияпирамиды вмассиве. Поместитекорневой узелв 1 позицию массива.Потомки узлаI размещаютсяв позициях 2 *I и 2 * I + 1. Рис. 9.7 показываетпирамиду с рис.9.6, записаннуюв виде массива.
Чтобыпонять, какустроена пирамида, заметим, чтопирамида созданаиз пирамидменьшего размера.Поддерево, начинающеесяс любого узлапирамиды, такжеявляется пирамидой.Например, впирамиде, показаннойна рис. 9.8, поддеревос корнем в узле13 также являетсяпирамидой.
Используяэтот факт, можнопостроитьпирамиду снизувверх. Вначале, разместимэлементы в видедерева, какпоказано нарис. 9.9. Затеморганизуемпирамиды изнебольшихподдеревьеввнизу дерева.Поскольку вних всего потри узла, сделатьэто достаточнопросто. Сравнимвершину с каждымиз потомков.Если один изпотомков больше, он меняетсяместами с родителем.Если оба потомкабольше, большийпотомок меняетсяместами с родителем.Этот шаг повторяетсядо тех пор, покавсе поддеревья, имеющие по 3узла, не будутпреобразованыв пирамиды, какпоказано нарис. 9.10.
Теперьобъединиммаленькиепирамиды длясоздания болеекрупных пирамид.Соединим нарис. 9.10 маленькиепирамиды свершинами 15 и5 и элемент, создавпирамиду большегоразмера. Сравнимновую вершину7 с каждым изпотомков. Еслиодин из потомковбольше, поменяемего местамис вершиной. Внашем случае15 больше, чем7 и 4, поэтому узел15 меняется местамис узлом 7.
Посколькуправое поддерево, начинающеесяс узла 4, не изменялось, это поддеревопо прежнемуявляется пирамидой.Левое же поддеревоизменилось.Чтобы определить, является лионо все ещепирамидой, сравним егоновую вершину7 с потомками13 и 12. Поскольку13 больше, чем7 и 12, необходимопоменять местамиузлы 7 и 13.

@Рис.9.7. Представлениепирамиды в видемассива

========248

@Рис.9.8. Пирамидаобразуетсяиз меньшихпирамид

@Рис.9.9. Неупорядоченныйсписок в полномдереве

@Рис.9.10. Поддеревьявторого уровняявляются пирамидами

=========249

@Рис.9.11. Объединениепирамид в пирамидубольшего размера

Еслиподдерево выше, можно продолжитьперемещениеузла 7 вниз поподдереву. Вконце концов, либо будетдостигнутаточка, в которойузел 7 большеобоих своихпотомков, либоалгоритм достигнетоснованиядерева. На рис.9.11 показано деревопосле преобразованияэтого поддеревав пирамиду.
Продолжимобъединениепирамид, образуяпирамиды большегоразмера до техпор, пока всеэлементы необразуют однубольшую пирамиду, такую как нарис. 9.6.
Следующийкод перемещаетэлемент изположенияList(min)вниз по пирамиде.Если поддеревьяниже List(min)являются пирамидами, то процедурасливает пирамиды, образуя пирамидубольшего размера.

PrivateSub HeapPushDown(List() s Long, ByVal min As Long, _
ByValmax As Long)
Dimtmp As Long
Dimj As Long

tmp= List(min)
Do
j= 2 * min
Ifj
‘Разместитьв j указательна большегопотомка.
Ifj
IfList(j + 1) > List(j) Then _
j= j + 1
EndIf

IfList(j) > tmp Then
‘Потомок больше.Поменять егоместами с родителем.
List(min)= List(j)
‘Перемещениеэтого потомкавниз.
min= j
Else
‘Родитель больше.Процедуразакончена.
ExitDo
EndIf
Else
ExitDo
EndIf
Loop
List(min)= tmp
EndSub

Полныйалгоритм, использующийпроцедуруHeapPushDownдля созданияпирамиды издерева элементов, необычайнопрост:

PrivateSub BuildHeap()
Dimi As Integer

Fori = (max + min) \ 2 To min Step -1
HeapPushDownlist(), i, max
Nexti
EndSub
Приоритетныеочереди
Приоритетнойочередью (priorityqueue) легкоуправлять припомощи процедурBuildHeapи HeapPushDown.Если в качествеприоритетнойочереди используетсяпирамида, легконайти элементс самым высокимприоритетом —он всегда находитсяна вершинепирамиды. Ноесли его удалить, получившеесядерево безкорня уже небудет пирамидой.
Длятого, чтобыснова превратитьдерево безкорня в пирамиду, возьмем последнийэлемент (самыйправый элементна нижнем уровне)и поместим егона вершинупирамиды. Затемпри помощипроцедурыHeapPushDownпродвинем новыйкорневой узелвниз по деревудо тех пор, покадерево сноване станет пирамидой.В этот момент, можно получитьна выходеприоритетнойочереди следующийэлемент с наивысшимприоритетом.

PublicFunction Pop() As Long
IfNumInQueue

'Удалить верхнийэлемент.
Pop= Pqueue(1)

'Переместитьпоследнийэлемент навершину.
PQueue(1)= PQueue(NumInPQueue)
NumInPQueue= NumInPQueue — 1

'Снова сделатьдеревопирамидой.
HeapPushDownPQueue(), 1, NumInPQueue
EndFunction

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

PrivateSub HeapPushUp(List() As Long, ByValmax As Integer)
Dimtmp As Long
Dimj As Integer

tmp= List (max)
Do
j= max \ 2
Ifj
IfList(j)
List(max) = List(j)
max= j
Else
ExitDo
EndIf
Loop
List(max)= tmp
EndSub

ПодпрограммаPushдобавляет новыйэлемент к деревуи используетподпрограммуHeapPushDownдля восстановленияпирамиды.

PublicSub Push (value As Long)
NumInPQueue= NumInPQueue + 1
IfNumInPQueue > PQueueSize ThenResizePQueue

PQueue(NumInPQueue)= value
HeapPushUpPQueue(), NumInPQueue
EndSub

========252
Анализпирамид
Припервоначальномпревращениисписка в пирамиду, это осуществляетсяпри помощисоздания множествапирамид меньшегоразмера. Длякаждого внутреннегоузла деревастроится пирамидас корнем в этомузле. Если деревосодержит N элементов, то в деревеO(N) внутреннихузлов, и в итогеприходитсясоздать O(N) пирамид.
Присоздании каждойпирамиды можетпотребоватьсяпродвигатьэлемент внизпо пирамиде, возможно дотех пор, покаон не достигнетконцевого узла.Самые высокиеиз построенныхпирамид будутиметь высотупорядка O(log(N)).Так как создаетсяO(N) пирамид, и дляпостроениясамой высокойиз них требуетсяO(log(n)) шагов, то все пирамидыможно построитьза время порядкаO(N * log(N)).
На самомделе временипотребуетсяеще меньше.Только некоторыепирамиды будутиметь высотупорядка O(log(N)).Большинствоиз них гораздониже. Толькоодна пирамидаимеет высоту, равную log(N), и половинапирамид — высотувсего в 2 узла.Если суммироватьвсе шаги, необходимыедля созданиявсех пирамид, в действительностипотребуетсяне больше O(N) шагов.
Чтобыувидеть, такли это, допустим, что деревосодержит N узлов.Пусть H — высотадерева. Этополное двоичноедерево, следовательно,H=log(N).
Теперьпредположим, что вы строитевсе большиеи большие пирамиды.Для каждогоузла, которыйнаходится нарасстоянииH-I уровней откорня дерева, строится пирамидас высотой I. Всеготаких узлов2H-I, и всегосоздается 2H-Iпирамид с высотойI.
Дляпостроенияэтих пирамидможет потребоватьсяпередвигатьэлемент вниздо тех пор, покаон не достигнетконцевого узла.Перемещениеэлемента внизпо пирамидес высотой I требуетдо I шагов. Дляпирамид с высотойI полное числошагов, котороепотребуетсядля построения2H-I пирамид, равно I*2H-I.
Сложиввсе шаги, затрачиваемыена построениепирамид разногоразмера, получаем1*2H-1+2*2H-2+3*2H-3+…+(H-1)*21. Вынеся заскобки 2H, получим2H*(1/2+2/22+3/23+…+(H-1)/2H-1).
Можнопоказать, что(1/2+2/22+3/23+…+(H-1)/2H-1)меньше 2. Тогдаполное числошагов, котороенужно для построениявсех пирамид, меньше, чем2H*2. Так как H —высота дерева, равная log(N), то полное числошагов меньше, чем 2log(N)*2=N*2. Этоозначает, чтодля первоначальногопостроенияпирамиды требуетсяпорядка O(N) шагов.
Дляудаления элементаиз приоритетнойочереди, последнийэлемент перемещаетсяна вершинудерева. Затемон продвигаетсявниз, пока незаймет своеокончательноеположение, идерево сноване станет пирамидой.Так как деревоимеет высотуlog(N), процессможет занятьне более log(N)шагов. Это означает, что новый элементк приоритетнойочереди наоснове пирамидыможно добавитьза O(log(N)) шагов.
Другимспособом работыс приоритетнымиочередямиявляетсяиспользованиеупорядоченногосписка. Вставкаили удалениеэлемента изупорядоченногосписка с миллиономэлементовзанимает примерномиллион шагов.Вставка илиудаление элементаиз сопоставимойпо размерамприоритетнойочереди, основаннойна пирамиде, занимает всего20 шагов.

======253
Алгоритмпирамидальнойсортировки
Алгоритмпирамидальнойсортировкипросто используетуже описанныеалгоритмы дляработы с пирамидами.Идея состоитв том, чтобысоздать приоритетнуюочередь ипоследовательноудалять поодному элементуиз очереди.
Дляудаления элементаалгоритм меняетего местамис последнимэлементом впирамиде. Этопомещает удаленныйэлемент в конечноеположение вконце массива.Затем алгоритмуменьшаетсчетчик элементовсписка, чтобыисключить израссмотренияпоследнююпозицию
Послетого, как наибольшийэлемент поменялсяместами с последним, массив большене являетсяпирамидой, таккак новый элементна вершинеможет оказатьсяменьше, чем егопотомки. Поэтомуалгоритм используетпроцедуруHeapPushDownдля продвиженияэлемента наего место. Алгоритмпродолжаетменять элементыместами ивосстанавливатьпирамиду дотех пор, покав пирамиде неостанетсяэлементов.

PublicSub Heapsort(List() As Long, ByValmin As Long,ByVal max AsLong)
Dimi As Long
Dimtmp As Long

'Создать пирамиду(кроме корневогоузла).
Fori = (max + min) \ 2 To min + 1 Step -1
HeapPushDownList(), i, max
Nexti

'Повторять:
' 1.Продвинутьсявниз по пирамиде.
' 2.Выдать корень.
Fori = max To min + 1 Step -1
'Продвинутьсявниз по пирамиде.
HeapPushDownList(), min, i

'Выдать корень.
tmp= List(min)
List(min)= List(i)
List(i)= tmp
Nexti
EndSub

Предыдущееобсуждениеприоритетныхочередей показало, что первоначальноепостроениепирамиды требуетO(N) шагов. Послеэтого требуетсяO(log(N)) шаговдля восстановленияпирамиды, когдаэлемент продвигаетсяна свое место.Пирамидальнаясортировкавыполняет этодействие N раз, поэтому требуетсявсего порядкаO(N)*O(log(N))=O(N*log(N))шагов, чтобыполучить изпирамидыупорядоченныйсписок. Полноевремя выполнениядля алгоритмапирамидальнойсортировкисоставляетпорядкаO(N)+O(N*log(N))=O(N*log(N)).

=========254

Такойже порядоксложности имееталгоритм сортировкислиянием и всреднем алгоритмбыстрой сортировки.Так же, как исортировкаслиянием, пирамидальнаясортировкатоже не зависитот значенийили распределенияэлементов доначала сортировки.Быстрая сортировкаплохо работаетсо списками, содержащимибольшое числоодинаковыхэлементов, асортировкаслиянием ипирамидальнаясортировкалишены этогонедостатка.
Хотяобычно пирамидальнаясортировкаработает немногомедленнее, чемсортировкаслиянием, длянее не требуетсядополнительногопространствадля хранениявременныхзначений, какдля сортировкислиянием.Пирамидальнаясортировкасоздает первоначальнуюпирамиду иупорядочиваетэлементы впределах исходногомассива списка.Сортировкаподсчетом
Сортировкаподсчетом(countingsort) —специализированныйалгоритм, которыйочень хорошоработает, еслиэлементы данных —целые числа, значения которыхнаходятся вотносительноузком диапазоне.Этот алгоритмработает достаточнобыстро, например, если значениянаходятся между1 и 1000.
Еслисписок удовлетворяетэтим требованиям, сортировкаподсчетомвыполняетсяневероятнобыстро. В одномиз тестов накомпьютерес процессоромPentium с тактовойчастотой 90 МГц, быстрая сортировка100.000 элементовсо значениямимежду 1 и 1000 заняла24,44 секунды. Длясортировкитех же элементовсортировкеподсчетомпотребовалосьвсего 0,88 секунд —в 27 раз меньшевремени.
Выдающаясяскорость сортировкиподсчетомдостигаетсяза счет того, что при этомне используютсяоперации сравнения.Ранее в этойглаве отмечалось, что время выполнениялюбого алгоритмасортировки, использующегооперации сравнения, порядка O(N*log(N)).Без использованияопераций сравнения, алгоритм сортировкиподсчетомпозволяетупорядочиватьэлементы завремя порядкаO(N).
Сортировкаподсчетомначинаетсяс созданиямассива дляподсчета числаэлементов, имеющих определенноезначение. Еслизначения находятсяв диапазонемежду min_valueи max_value, алгоритм создаетмассив Countsс нижней границейmin_valueи верхней границейmax_value.Если используетсямассив из предыдущегопрохода, необходимообнулить значенияего элементов.Если существуетM значений элементов, массив содержитM записей, и времявыполненияэтого шагапорядка O(M).

Fori = min To max
Counts(List(i))= Counts(List(i)) + 1
Nexti

В концеконцов, алгоритмобходит массивCounts, помещая соответствующеечисло элементовв отсортированныймассив. Длякаждого значенияiмежду min_valueи max_value, он помещаетCounts(i)элементов созначением iв массив. Таккак этот шагпомещает поодной записив каждую позициюв массиве, онтребует порядкаO(N) шагов.

new_index= min
Fori = min_value To max_value
Forj = 1 To Counts(i)
sorted_list(new_index)= i
new_index= new_index + 1
Nextj
Nexti

======255

Алгоритмцеликом требуетпорядкаO(M)+O(N)+O(N)=O(M+N) шагов. ЕслиM мало по сравнениюс N, он выполняетсяочень быстро.Например, еслиM
С другойстороны, еслиM больше, чемO(N*log(N)), тогда O(M+N) будетбольше, чемO(N*log(N)). Вэтом случаесортировкаподсчетом можетоказатьсямедленнее, чемалгоритмы сосложностьюпорядка O(N*log(N)), такие как быстраясортировка.В одном из тестовбыстрая сортировка1000 элементовсо значениямиот 1 до 500.000 потребовал0,054 сек, в то времякак сортировкаподсчетомпотребовала1,76 секунд.
Сортировкаподсчетомопирается натот факт, чтозначения данных —целые числа, поэтому этоталгоритм неможет простосортироватьданные другихтипов. В VisualBasic нельзясоздать массивс границамиот AAA до ZZZ.
Ранеев этой главев разделе«объединениеи сжатие ключей»было продемонстрировано, как можно кодироватьстроковыеданные припомощи целыхчисел. Если выможет закодироватьданные припомощи данныхтипа Integerили Long, вы все еще можетеиспользоватьсортировкуподсчетом.Блочнаясортировка
Как исортировкаподсчетом,блочная сортировка(bucketsort) не используетопераций сравненияэлементов. Этоталгоритм используетзначения элементовдля разбиенияих на блоки, изатем рекурсивносортируетполученныеблоки. Когдаблоки становятсядостаточномалыми, алгоритмостанавливаетсяи используетболее простойалгоритм типасортировкивыбором длязавершенияпроцесса.
По смыслуэтот алгоритмпохож на быструюсортировку.Быстрая сортировкаразделяетэлементы надва подспискаи рекурсивносортируетподсписки.Блочная сортировкаделает то жесамое, но делитсписок на множествоблоков, а не навсего лишь дваподсписка.
Дляделения спискана блоки, алгоритмпредполагает, что значенияданных распределеныравномерно, и распределяетэлементы поблокам равномерно.Например, предположим, что данныеимеют значенияв диапазонеот 1 до 100 и алгоритмиспользует10 блоков. Алгоритмпомещает элементысо значениями1 10 в первый блок, со значениями11 20 — во второй, и т.д. На рис. 9.12показан списокиз 10 элементовсо значениямиот 1 до 100, которыерасположеныв 10 блоках.

@Рис.9.12. Расположениеэлементов вблоках.

=======256

Еслиэлементы распределеныравномерно, в каждый блокпопадает примерноодинаковоечисло элементов.Если в спискеN элементов, иалгоритм используетN блоков, в каждыйблок попадаетвсего один илидва элемента.Программа можетотсортироватьих за конечноечисло шагов, поэтому времявыполненияалгоритма вцелом порядкаO(N).
На практике, распределениеданных обычноне являетсяравномерным.В некоторыеблоки попадаетбольше элементов, в другие меньше.Тем не менее, если распределениев целом близкок равномерному, то в каждом изблоков окажетсялишь небольшоечисло элементов.
Проблемымогут возникать, только еслисписок содержитнебольшое числоразличныхзначений. Например, если все элементыимеют одно ито ж значение, они все будутпомещены в одинблок. Если алгоритмне обнаружитэто, он сноваи снова будетпомещать всеэлементы в одини тот же блок, вызвав бесконечнуюрекурсию иисчерпав всестековоепространство.Блочнаясортировкас применениемсвязного списка
Реализоватьалгоритм блочнойсортировкина Visual Basicможно различнымиспособами.Во-первых, можноиспользоватьв качествеблоков связныесписки. Этооблегчаетперемещениеэлементов междублоками в процессеработы алгоритма.
Этотметод можетбыть болеесложным, еслиэлементы изначальнорасположеныв массиве. Вэтом случае, необходимоперемещатьэлементы измассива в связныйсписок и обратнов массив послезавершениясортировки.Для созданиясвязного спискатакже требуетсядополнительнаяпамять. Следующийкод демонстрируеталгоритм блочнойсортировкис применениемсвязных списков:
    продолжение
--PAGE_BREAK--
PublicSub LinkBucketSort(ListTop As ListCell)
Dimcount As Long
Dimmin_value As Long
Dimmax_value As Long
DimValue As Long
Dimitem As ListCell
Dimnxt As ListCell
Dimbucket() As New ListCell
Dimvalue_scale As Double
Dimbucket.num AsLong
Dimi As Long

Setitem = ListTop.NextCell
Ifitem Is Nothing Then Exit Sub

'Подсчитатьэлементы инайти значенияmin и max.
count= 1
min_value= item.Value
max_value= min_value
Setitem = item.NextCell
DoWhile Not (item Is Nothing)
count= count + 1
Value= item.Value
Ifmin_value > Value Then min_value = Value
Ifmax_value
Setitem = item.NextCell
Loop

'Если min_value = max_value, значит, есть единственное
'значение, исписок отсортирован.
Ifmin_value = max_value Then Exit Sub

'Если в спискене более, чемCutOff элементов,
'завершитьсортировкупроцедуройLinkInsertionSort.
Ifcount
LinkInsertionSortListTop
ExitSub
EndIf

'Создать пустыеблоки.
ReDimbucket(1 To count)

value_scale= _
CDbl(count- 1) / _
CDbl(max_value- min_value)

'Разместитьэлементы вблоках.
Setitem = ListTop.NextCell
DoWhile Not (item Is Nothing)
Setnxt = item.NextCell
Value= item.Value
IfValue = max_value Then
bucket_num= count
Else
bucket_num= _
Int((Value- min_value) * _
value_scale)+ 1
EndIf
Setitem.NextCell = bucket (bucket_num).NextCell
Setbucket(bucket_num).NextCell = item
Setitem = nxt
Loop

'Рекурсивнаясортировкаблоков, содержащих
'более одногоэлемента.
Fori = 1 To count
IfNot (bucket(i).NextCell Is Nothing) Then _
LinkBucketSortbucket(i)
Nexti

'Объединитьотсортированныесписки.
SetListTop.NextCell = bucket(count).NextCell
Fori = count — 1 To 1 Step -1
Setitem = bucket(i).NextCell
IfNot (item Is Nothing) Then
DoWhile Not (item.NextCell Is Nothing)
Setitem = item.NextCell
Loop
Setitem.NextCell = ListTop.NextCell
SetListTop.NextCell= bucket(i).NextCell
EndIf
Nexti
EndSub

=========257-258

Этаверсия блочнойсортировкинамного быстрее, чем сортировкавставкой сиспользованиемсвязных списков.В тесте на компьютерес процессоромPentium с тактовойчастотой 90 МГцсортировкевставкойпотребовалось6,65 секунд длясортировки2000 элементов, блочная сортировказаняла 1,32 секунды.Для более длинныхсписков разницабудет еще больше, так как производительностьсортировкивставкой порядкаO(N2).Блочнаясортировкана основе массива
Блочнуюсортировкутакже можнореализоватьв массиве, используяидеи подобныетем, которыеиспользуютсяпри сортировкеподсчетом. Прикаждом вызовеалгоритма, вначале подсчитываетсячисло элементов, которые относятсяк каждому блоку.Потом на основеэтих данныхрассчитываютсясмещения вовременноммассиве, которыезатем используютсядля правильногорасположенияэлементов вмассиве. В концеконцов, блокирекурсивносортируются, и отсортированныеданные перемещаютсяобратно в исходныймассив.

PublicSub ArrayBucketSort(List() As Long, Scratch() As Long, _
minAs Long, max As Long, NumBuckets As Long)
Dimcounts() As Long
Dimoffsets() As Long

Dimi As Long
DimValue As Long
Dimmin_value As Long
Dimmax_value As Long
Dimvalue_scale As Double
Dimbucket_num As Long
Dimnext_spot As Long
Dimnum_in_bucket As Long

'Если в спискене более чемCutOff элементов,
'закончитьсортировкупроцедуройSelectionSort.
Ifmax — min + 1
SelectionsortList(), min, max
ExitSub
EndIf

'Найти значенияmin и max.
min_value= List(min)
max_value= min_value
Fori = min + 1 To max
Value= List(i)
Ifmin_value > Value Then min_value = Value
Ifmax_value
Nexti

'Если min_value = max_value, значит, есть единственное
'значение, исписок отсортирован.
Ifmin_value = max_value Then Exit Sub

'Создать пустоймассив с отсчетамиблоков.
ReDimcounts(l To NumBuckets)

value_scale= _
CDbl(NumBuckets — 1) / _
CDbl(max_value — min_value)

'Создать отсчетыблоков.
Fori = min To max
IfList(i) = max_value Then
bucket_num= NumBuckets
Else
bucket_num= _
Int((List(i)- min_value) * _
value_scale)+ 1
EndIf
counts(bucket_num)= counts(bucket_num) + 1
Nexti

'Преобразоватьотсчеты в смещениев массиве.
ReDimoffsets(l To NumBuckets)
next_spot= min
Fori = 1 To NumBuckets
offsets(i)= next_spot
next_spot= next_spot + counts(i)
Nexti

'Разместитьзначения всоответствующихблоках.
Fori = min To max
IfList(i) = max_value Then
bucket_num= NumBuckets
Else
bucket_num= _
Int((List(i)- min_value) * _
value_scale)+ 1
EndIf
Scratch(offsets (bucket_num)) = List(i)
offsets(bucket_num)= offsets(bucket_num) + 1
Nexti

'Рекурсивнаясортировкаблоков, содержащих
'более одногоэлемента.
next_spot= min
Fori = 1 To NumBuckets
Ifcounts(i) > 1 Then ArrayBucketSort _
Scratch(),List(), next_spot, _
next_spot+ counts(i) — 1, counts(i)
next_spot= next_spot + counts(i)
Nexti

'Скопироватьвременныймассив назадв исходныйсписок.
Fori = min To max
List(i)= Scratch(i)
Nexti
EndSub

Из занакладныхрасходов, которыетребуются дляработы со связнымисписками, этаверсия блочнойсортировкиработает намногобыстрее, чемверсия с использованиемсвязных списков.Тем не менее, используяметоды работыс псевдоуказателями, описанные во2 главе, можноулучшитьпроизводительностьверсии с использованиемсвязных списков, так что обеверсии станутпрактическиэквивалентнымипо скорости.
Новуюверсию такжеможно сделатьеще быстрее, используяфункцию API MemCopyдля копированияэлементов извременногомассива обратнов исходныйсписок. Этаусовершенствованнуюверсию алгоритмадемонстрируетпрограммаFastSort.

===========259-261
Резюме
В таб.9.4 приведеныпреимуществаи недостаткиалгоритмовсортировки, описанных вэтой главе, изкоторых можновывести несколькоправил, которыемогут помочьвам выбратьалгоритм сортировки.
Этиправила, изложенныев следующемсписке, и информацияв табл. 9.4 можетпомочь вамподобратьалгоритм, которыйобеспечитмаксимальнуюпроизводительность:
если вам нужно быстро реализовать алгоритм сортировки, используйте быструю сортировку, а затем при необходимости поменяйте алгоритм;
если более 99 процентов списка уже отсортировано, используйте пузырьковую сортировку;
если список очень мал (100 или менее элементов), используйте сортировку выбором;
если значения находятся в связном списке, используйте блочную сортировку на основе связного списка;
если элементы в списке — целые числа, разброс значений которых невелик (до нескольких тысяч), используйте сортировку подсчетом;
если значения лежат в широком диапазоне и не являются целыми числами, используйте блочную сортировку на основе массива;
если вы не можете тратить дополнительную память, которая требуется для блочной сортировки, используйте быструю сортировка
Есливы знаете структуруданных и различныеалгоритмысортировки, вы можете выбратьалгоритм, наиболееподходящийдля ваших нужд.

@Таблица9.4. Преимуществаи недостаткиалгоритмовсортировки

=========263
Глава10. Поиск
Послетого, как списокэлементовотсортирован, может понадобитьсянайти определенныйэлемент в списке.В этой главеописаны некоторыеалгоритмы дляпоиска элементовв упорядоченныхсписках. Онаначинаетсяс краткогоописания сортировкиметодом полногоперебора. Хотяэтот алгоритмвыполняетсяне так быстро, как другие, метод полногоперебора являетсяочень простым, что облегчаетего реализациюи отладку. Из запростоты этогометода, сортировкаполным переборомтакже выполняетсябыстрее другихалгоритмовдля очень маленькихсписков.
Далеев главе описандвоичный поиск.При двоичномпоиске списокмногократноразбиваетсяна части, приэтом для большихсписков такойпоиск выполняетсянамного быстрее, чем полныйперебор. Заключеннаяв этом методеидея достаточнопроста, нореализоватьее довольносложно.
Затемв главе описанинтерполяционныйпоиск. Так же, как и в методедвоичногопоиска, исходныйсписок при этоммногократноразбиваетсяна части. Прииспользованииинтерполяционногопоиска, алгоритмделает предположенияо том, где можетнаходитьсяискомый элемент, поэтому онвыполняетсянамного быстрее, если данныев спискахраспределеныравномерно.
В концеглавы обсуждаютсяметоды следящегопоиска. Применениеэтого методаиногда уменьшаетвремя поискав несколькораз.Примерыпрограмм
ПрограммаSearchдемонстрируетвсе описанныев главе алгоритмы.Введите значениеэлементов, которые долженсодержатьсписок, и затемнажмите накнопку MakeList (Создатьсписок), и программасоздаст списокна основе массива, в котором каждыйэлемент большепредыдущегона число от 0до 5. Программавыводит значениенаибольшегоэлемента всписке, чтобывы представлялидиапазон значенийэлементов.
Послесоздания спискавыберите алгоритмы, которые выхотите использовать, установивсоответствующиефлажки. Затемвведите значение, которое выхотите найтии нажмите накнопку Search(Поиск), и программавыполнит поискэлемента припомощи выбранноговами алгоритма.Так как списоксодержит невсе возможныеэлементы взаданном диапазонезначений, товам можетпонадобитьсяввести несколькоразличныхзначений, преждечем одно из нихнайдется всписке.
Программатакже позволяетзадать числоповторенийдля каждогоиз алгоритмовпоиска. Некоторыеалгоритмывыполняютсяочень быстро, поэтому длятого, чтобысравнить ихскорость, можетпонадобитьсязадать для нихбольшое числоповторений.

=======265

На рис.10.1 показано окнопрограммыSearchпосле поискаэлемента созначением250.000. Этот элементнаходился напозиции 99.802 всписке из 100.000элементов.Чтобы найтиэтот элемент, потребовалосьпроверить99.802 элемента прииспользованииалгоритмаполного перебора,16 элементов —при использованиидвоичногопоиска и всего3 — при выполненииинтерполяционногопоиска.Поискметодом полногоперебора
Привыполнениилинейного(linear) поискаили поискаметодом полногоперебора(exhaustive search), поиск ведетсяс начала списка, и элементыперебираютсяпоследовательно, пока среди нихне будет найденискомый.

PublicFunction LinearSearch(target As Long) AsLong
Dimi As Long

Fori = 1 To NumItems
IfList(i) >= target Then Exit For
Nexti

Ifi > NumItems Then
Search= 0 ' Элементне найден.
Else
Search= i 'Элементнайден.
EndIf
EndFunction

Таккак этот алгоритмпроверяетэлементыпоследовательно, то он находитэлементы вначале спискабыстрее, чемэлементы, расположенныев конце. Наихудшийслучай дляэтого алгоритмавозникает, еслиэлемент находитсяв конце спискаили вообще неприсутствуетв нем. В этихслучаях, алгоритмпроверяет всеэлементы всписке, поэтомувремя его выполнениясложность внаихудшемслучае порядкаO(N).

@Рис.10.1. ПрограммаSearch

========266

Еслиэлемент находитсяв списке, то всреднем алгоритмпроверяет N/2элементов дотого, как обнаружитискомый. Такимобразом, вусредненномслучае времявыполненияалгоритма такжепорядка O(N).
Хотяалгоритмы, которые выполняютсяза время порядкаO(N), неявляются оченьбыстрыми, этоталгоритм достаточнопрост, чтобыдавать на практикенеплохие результаты.Для небольшихсписков этоталгоритм имеетприемлемуюпроизводительность.Поискв упорядоченныхсписках
Еслисписок упорядочен, то можно слегкамодифицироватьалгоритм полногоперебора, чтобынемного повыситьего производительность.В этом случае, если во времявыполненияпоиска алгоритмнаходит элементсо значением, большим, чемзначение искомогоэлемента, тоон завершаетсвою работу.При этом искомыйэлемент ненаходится всписке, так какиначе он бывстретилсяраньше.
Например, предположим, что мы ищемзначение 12 идошли до значения17. При этом мыуже прошли тотучасток списка, в котором могбы находитсяэлемент созначением 12, значит, элемент12 в списке отсутствует.Следующий коддемонстрируетдоработаннуюверсию алгоритмапоиска полнымперебором:

PublicFunction LinearSearch(target As Long) As Long
Dimi As Long

NumSearches= 0

Fori = 1 To NumItems
NumSearches= NumSearches + 1
IfList(i) >= target Then Exit For
Nexti

Ifi > NumItems Then
LinearSearch= 0 ' Элементне найден.
ElseIfList(i) target Then
LinearSearch= 0 ' Элементне найден.
Else
LinearSearch= i ' Элементнайден.
EndIf
EndFunction

Этамодификацияуменьшает времявыполненияалгоритма, еслиэлемент отсутствуетв списке. Предыдущейверсии поискатребовалосьпроверить весьсписок до конца, если искомогоэлемента в немне было. Новаяверсия остановится, как толькообнаружитэлемент больший, чем искомый.
Еслиискомый элементрасположенслучайно междунаибольшими наименьшимэлементамив списке, то всреднем алгоритмупонадобитсяпорядка O(N)шагов, чтобыопределить, что искомыйэлемент отсутствуетв списке. Времявыполненияпри этом имееттот же порядок, но на практикеего производительностьбудет немноговыше. ПрограммаSearchиспользуетэту версиюалгоритма.

======267
Поискв связных списках
Поискметодом полногоперебора —это единственныйспособ поискав связных списках.Так как доступк элементамвозможен толькопри помощиуказателейNextCellна следующийэлемент, тонеобходимопроверить поочереди всеэлементы сначала списка, чтобы найтиискомый.
Также, как и в случаепоиска полнымперебором вмассиве, еслисписок упорядочен, то можно прекратитьпоиск, еслинайдется элементсо значением, большим, чемзначение искомогоэлемента.

PublicFunction LListSearch(target As Long) As SearchCell
Dimcell As SearchCell

NumSearches= 0
Setcell = ListTop.NextCell
DoWhile Not (cell Is Nothing)
NumSearches= NumSearches + 1

Ifcell.Value >= target Then Exit Do
Setcell = cell.NextCell
Loop
IfNot (cell Is Nothing) Then
Ifcell.Value = target Then
SetLListSearch = cell ' Элементнайден.
EndIf
EndIf
EndFunction

ПрограммаSearchиспользуетэтот алгоритмдля поискаэлементов всвязном списке.Этот алгоритмвыполняетсянемного медленнее, чем алгоритмполного переборав массиве из задополнительныхнакладныхрасходов, которыесвязаны с управлениемуказателямина объекты.Заметьте, чтопрограммаSearchстроит связныесписки, толькоесли списоксодержит неболее 10.000 элементов.
Чтобыалгоритм выполнялсянемного быстрее, в него можновнести еще одноизменение. Еслихранить указательна конец списка, то можно добавитьв конец спискаячейку, котораябудет содержатьискомый элемент.Этот элементназываетсясигнальнойметкой (sentinel), и служит длятех же целей, что и сигнальныеметки, описанныево 2 главе. Этопозволяетобрабатыватьособый случайконца спискатак же, как ивсе остальные.
В этомслучае, добавлениеметки в конецсписка гарантирует, что в концеконцов искомыйэлемент будетнайден. Приэтом программане может выйтиза конец списка, и нет необходимостипроверятьусловие Not(cellIsNothing)в каждом циклеWhile.

PublicFunction SentinelSearch(target As Long) As SearchCell
Dimcell As SearchCell
Dimsentinel As New SearchCell

NumSearches= 0

'Установитьсигнальнуюметку.
sentinel.Value= target
SetListBottom.NextCell = sentinel
'Найти искомыйэлемент.
Setcell = ListTop.NextCell
DoWhile cell.Value
NumSearches= NumSearches + 1
Setcell = cell.NextCell
Loop

'Определитьнайден ли искомыйэлемент.
IfNot ((cell Is sentinel) Or _
(cell.Value target)) _
Then
SetSentinelSearch = cell ' Элементнайден.
EndIf

'Удалить сигнальнуюметку.
SetListBottom.NextCell = Nothing
EndFunction

Хотяможет показаться, что это изменениенезначительно, проверка Not(cellIsNothing)выполняетсяв цикле, которыйвызываетсяочень часто.Для большихсписков этотцикл вызываетсямножество раз, и выигрыш временисуммируется.В Visual Basic, этот версияалгоритмапоиска в связныхсписках выполняетсяна 20 процентовбыстрее, чемпредыдущаяверсия. В программеSearchприведены обеверсии этогоалгоритма, ивы можете сравнитьих.
Некоторыеалгоритмыиспользуютпотоки дляускоренияпоиска в связныхсписках. Например, при помощиуказателейв ячейках спискаможно организоватьсписок в видедвоичногодерева. Поискэлемента сиспользованиемэтого деревазаймет времяпорядка O(log(N)), если деревосбалансировано.Такие структурыданных уже неявляются простосписками, поэтомумы не обсуждаемих в этой главе.Чтобы большеузнать о деревьях, обратитеськ 6 и 7 главамДвоичныйпоиск
Какуже упоминалосьв предыдущихразделах, поискполным переборомвыполняетсяочень быстродля небольшихсписков, но длябольших списковнамного быстреевыполняетсядвоичный поиск.Алгоритм двоичногопоиска (binarysearch) сравниваетэлемент в серединесписка с искомым.Если искомыйэлемент меньше, чем найденный, то алгоритмпродолжаетпоиск в первойполовине списка, если больше —в правой половине.На рис. 10.2 этотпроцесс изображенграфически.
Хотяпо своей природеэтот алгоритмявляется рекурсивным, его достаточнопросто записатьи без применениярекурсии. Таккак этот алгоритмпрост для пониманияв любом варианте(с рекурсиейили без), то мыприводим здесьего нерекурсивнуюверсию, котораясодержит меньшевызовов функций.
Основнаязаключеннаяв этом алгоритмеидея проста, но детали еереализациидостаточносложны. Программеприходитсяаккуратноотслеживатьчасть массива, которая можетсодержатьискомый элемент, иначе она можетего пропустить.
Алгоритмиспользуетдве переменные,minи max, в которых находятсяминимальныйи максимальныйиндексы ячеекмассива, которыемогут содержатьискомый элемент.Во время выполненияалгоритма, индекс искомойячейки всегдабудет лежатьмежду minи max.Другими словами,min
    продолжение
--PAGE_BREAK--
SubSlow()
DimI As Integer
DimJ As Integer
DimK As Integer
ForI = 1 To N
ForJ = 1 To N
ForK = 1 To N
'Выполнитькакие либодействия.
NextK
NextJ
NextI
EndSub

SubFast()
DimI As Integer
DimJ As Integer
DimK As Integer
ForI = 1 To N
ForJ = 1 To N
Slow 'Вызов процедурыSlow.
NextJ
NextI
EndSub

SubMainProgram()
Fast
EndSub

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

SubSlow()
DimI As Integer
DimJ As Integer
DimK As Integer

ForI = 1 To N
ForJ = 1 To N
ForK = 1 To N
'Выполнитькакие либодействия.
NextK
NextJ
NextI
EndSub

SubFast()
DimI As Integer
DimJ As Integer
ForI = 1 To N
ForJ = 1 To N
'Выполнитькакие либодействия.
NextJ
NextI
EndSub

SubMainProgram()
Slow
Fast
EndSub

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

SubCountDown(N As Integer)
IfN
CountDownN — 1
EndSub

===========6
Многократнаярекурсия
Рекурсивныйалгоритм, вызывающийсебя несколькораз, являетсяпримероммногократнойрекурсии (multiplerecursion). Процедурыс множественнойрекурсиейсложнее анализировать, чем просторекурсивныеалгоритмы, иони могут даватьбольший вкладв общую сложностьалгоритма.
Нижеприведеннаяподпрограммапохожа на предыдущуюподпрограммуCountDown, только онавызывает самусебя дважды:

SubDoubleCountDown(N As Integer)
IfN
DoubleCountDownN — 1
DoubleCountDownN — 1
EndSub

Можнобыло бы предположить, что время выполненияэтой процедурыбудет в двараза больше, чем для подпрограммы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рекурсивныхвызовов, процедуразаймет всюсвободнуюпамять, и программааварийно остановитсяс ошибкой «Outof Memory».

SubGobbleMemory(N As Integer)
DimArray() As Integer

ReDimArray (1 To 32000)
GobbleMemoryN + 1
EndSub

Дажеесли внутрипроцедурыпамять незапрашивается, система выделяетпамять из системногостека (systemstack) для сохраненияпараметровпри каждомвызове процедуры.После возвратаиз процедурыпамять из стекаосвобождаетсядля дальнейшегоиспользования.
Еслив подпрограммевстречаетсядлинная последовательностьрекурсивныхвызовов, программаможет исчерпатьстек, даже есливыделеннаяпрограммепамять еще невся использована.Если запуститьна исполнениеследующуюподпрограмму, она быстроисчерпает всюсвободнуюстековую памятьи программааварийно прекратитработу с сообщениемоб ошибке «Outof stack Space».После этоговы сможетеузнать значениепеременнойCount, чтобы узнать, сколько разподпрограммавызывала себяперед тем, какисчерпать стек.

SubUseStack()
StaticCount As Integer

Count= Count + 1
UseStack
EndSub

Определениелокальныхпеременныхвнутри подпрограммытакже можетзанимать памятьиз стека. Еслиизменить подпрограммуUseStackиз предыдущегопримера так, чтобы она определялатри переменныхпри каждомвызове, программаисчерпаетстековое пространствоеще быстрее:

SubUseStack()
StaticCount As Integer
DimI As Variant
DimJ As Variant
DimK As Variant

Count= Count + 1
UseStack
EndSub

В 5 главерекурсивныеалгоритмыобсуждаютсяболее подробно.

==============8
Наихудшийи усредненныйслучай
Оценкас точностьюдо порядка даетверхний пределсложностиалгоритма. То, что программаимеет определенныйпорядок сложности, не означает, что алгоритмбудет действительновыполнятьсятак долго. Приопределенныхисходных данных, многие алгоритмывыполняютсягораздо быстрее, чем можнопредположитьна основанииих порядкасложности.Например, следующийкод реализуетпростой алгоритмвыбора элементаиз списка:

FunctionLocateItem(target As Integer) As Integer
ForI = 1 To N
IfValue(I) = target Then Exit For
NextI
LocateItem= I
EndSub

Еслиискомый элементнаходится вконце списка, придется перебратьвсе N элементовдля того, чтобыего найти. Этозаймет N шагов, значит сложностьалгоритмапорядка O(N). В этом, так называемомнаихудшемслучае (worstcase) времявыполненияалгоритма будетнаибольшим.
С другойстороны, еслиискомое числов начале списка, алгоритм завершитработу практическисразу, совершиввсего несколькоитераций. Этотак называемыйнаилучшийслучай (bestcase) со сложностьюпорядка 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отводит частьдисковогопространствапод виртуальнуюпамять (virtualmemory). Когдаисчерпываетсяоперативнаяпамять, Windowsсбрасываетчасть ее содержимогона диск. Освободившаясяпамять предоставляетсяпрограмме. Этотпроцесс называетсяподкачкой, посколькустраницы, сброшенныена диск, могутбыть подгруженысистемой обратнов память приобращении кним.
Посколькуоперации сдиском намногомедленнееопераций спамятью, слишкомчастое обращениек файлу подкачкиможет значительноснизить производительностьприложения.Если программачасто обращаетсяк большим объемампамяти, системабудет частоиспользоватьфайл подкачки, что приведетк замедлениюработы.
Приведеннаяв числе примеровпрограмма Pagerзапрашиваетвсе больше ибольше памятипод создаваемыемассивы до техпор, пока программане начнет обращатьсяк файлу подкачки.Введите количествопамяти в мегабайтах, которое программадолжна запросить, и нажмите кнопкуPage (Подкачка).Если ввестинебольшоезначение, например1 или 2 Мбайт, программасоздаст массивв оперативнойпамяти, и будетвыполнятьсябыстро.
Еслиже вы введетезначение, близкоек объему оперативнойпамяти вашегокомпьютера, то программаначнет использоватьфайл подкачки.Вполне вероятно, что она будетпри этом обращатьсяк диску постоянно.Вы также заметите, что программавыполняетсянамного медленнее.Увеличениеразмера массивана 10 процентовможет привестик 100 процентномуувеличениювремени исполнения.
ПрограммаPagerможет использоватьпамять однимиз двух способов.Если вы нажметекнопку Page,программаначнет последовательнообращатьсяк элементаммассива. Помере переходаот одной частимассива к другой, системе можетпотребоватьсяподгружатьих с диска. Послетого, как частьмассива оказаласьв памяти, программаможет продолжитьработу с ней.

============12

Еслиже вы нажметена кнопку Thrash(Пробуксовка), программа будетслучайно обращатьсяк разным участкампамяти. Приэтом вероятностьтого, что нужнаястраница находитсяв этот моментна диске, намноговозрастает.Это избыточноеобращение кфайлу подкачкиназываетсяпробуксовкойпамяти(thrashing). В табл.1.4 приведеновремя исполненияпрограммы Pagerна компьютерес процессоромPentium с тактовойчастотой 90 МГци 24 Мбайт оперативнойпамяти. В зависимостиот конфигурациивашего компьютера, скорости работыс диском, количестваустановленнойоперативнойпамяти, а такженаличия другихзапущенныхпараллельноприложенийвремя выполненияпрограммы можетсильно различаться.
Вначалевремя выполнениятеста растетпочти пропорциональноразмеру занятойпамяти. Когданачинаетсяобращение кфайлу подкачки, скорость работыпрограммы резкопадает. Заметьте, что до этоготесты с обращениемк файлу подкачкии пробуксовкойведут себяпрактическиодинаково, тоесть когда весьмассив находитсяв оперативнойпамяти, последовательноеи случайноеобращение кэлементаммассива занимаетодинаковоевремя. При подкачкеэлементовмассива с дискаслучайныйдоступ к памятинамного менееэффективен.
Дляуменьшениячисла обращенийк файлу подкачкиесть несколькоспособов. Основнойприем — экономноерасходованиепамяти. Приэтом надо помнить, что программаобычно не можетзанять всюфизическуюпамять, потомучто часть еезанимает системаи другие программы.Компьютер, накотором былиполучены результаты, приведенныев табл. 1.4, начиналинтенсивнообращатьсяк диску, когдапрограммазанимала 20 Мбайтиз 24 Мбайт физическойпамяти.
Иногдаможно написатькод так, чтопрограмма будетобращатьсяк блокам памятипоследовательно.Алгоритм сортировкислиянием, описанныйв 9 главе, манипулируетбольшими блокамиданных. Этиблоки сортируются, а затем сливаютсявместе. Упорядоченнаяработа с памятьюуменьшает числообращений кдиску.

@Таблица1.4. Время выполненияпрограммы Pagerв секундах
    продолжение
--PAGE_BREAK--
==========269

@Рис.10.2. Двоичный поискэлемента созначением 44

Во времякаждого прохода, алгоритм выполняетприсвоениеmiddle= (min+ max)/ 2 и проверяетячейку, индекскоторой равенmiddle.Если ее значениеравно искомому, то цель найденаи алгоритмзавершает своюработу.
Еслизначение искомогоэлемента меньше, чем значениесреднего, тоалгоритмустанавливаетзначение переменнойmaxравным middle– 1 и продолжаетпоиск. Так кактеперь индексыэлементов, которые могутсодержатьискомый элемент, находятся вдиапазоне отminдо middle– 1, топрограмма приэтом выполняетпоиск в первойполовине списка.
В концеконцов, программалибо найдетискомый элемент, либо наступитмомент, когдазначение переменнойminстанет больше, чем значениеmax.Посколькуиндекс искомогоэлемента долженнаходитьсямежду минимальными максимальнымвозможнымииндексами, этоозначает, чтоискомый элементотсутствуетв списке.
Следующийкод демонстрируетвыполнениедвоичногопоиска в программеSearch:

PublicFunction BinarySearch(target As Long) As Long
Dimmin As Long
Dimmax As Long
Dimmiddle As Long

NumSearches= 0

'Во время поискаиндекс искомогоэлемента будетнаходиться
'между Min иMax: Min
min= 1
max= NumItems
DoWhile min
NumSearches= NumSearches + 1
middle= (max + min) / 2
Iftarget = List(middle) Then ' Мынашли искомыйэлемент!
BinarySearch= middle
ExitFunction
ElseIftarget
max= middle — 1
Else 'Поиск в правойполовине.
min= middle + 1
EndIf
Loop
'Если мы оказалисьздесь, то искомогоэлемента нетв списке.
BinarySearch= 0
EndFunction

На каждомшаге числоэлементов, которые ещемогут иметьискомое значение, уменьшаетсявдвое. Для спискаразмера N, алгоритму можетпотребоватьсямаксимум O(log(N))шагов, чтобынайти любойэлемент илиопределить, что его нет всписке. Этонамного быстрее, чем в случаепримененияалгоритмаполного перебора.Полный переборсписка из миллионаэлементовпотребовалбы в среднем500.000 шагов. Алгоритмудвоичногопоиска потребуетсяне больше, чемlog(1.000.000) или 20шагов.Интерполяционныйпоиск
Двоичныйпоиск обеспечиваетзначительноеувеличениескорости поискапо сравнениюс полным перебором, так как он исключаетбольшие частисписка, не проверяяпри этом значенияисключаемыхэлементов.Если, крометого, известно, что значенияэлементовраспределеныдостаточноравномерно, то можно исключатьна каждом шагееще большеэлементов, используяинтерполяционныйпоиск (interpolationsearch).
Интерполяциейназываетсяпроцесс предсказаниянеизвестныхзначений наоснове имеющихся.В данном случае, индексы известныхзначений всписке используютсядля определениявозможногоположенияискомого элементав списке.
Например, предположим, что имеетсятот же самыйсписок значений, показанныйна рис. 10.2. Этотсписок содержит20 элементов созначениямимежду 1 и 70. Предположимтеперь, чтотребуется найтиэлемент в списке, имеющий значение44. Значение 44составляет64 процентарасстояниямежду 1 и 70 на шкалечисел. Еслисчитать, чтозначения элементовраспределеныравномерно, то можно предположить, что искомыйэлемент расположенпримерно вточке, котораясоставляет64 процента отразмера списка, и занимаетпозицию 13.
Еслипозиция, выбраннаяпри помощиинтерполяции, оказываетсянеправильной, то алгоритмсравниваетискомое значениесо значениемэлемента ввыбраннойпозиции. Еслиискомое значениеменьше, то поискпродолжаетсяв первой частисписка, еслибольше — вовторой части.На рис. 10.3 графическиизображенинтерполяционныйпоиск.
Придвоичном поискесписок последовательноразбиваетсяпосерединена две части.Интерполяционныйпоиск каждыйраз разбиваетсписок, пытаясьнайти ближайшийк искомомуэлемент в списке, при этом точкаразбиенияопределяетсяследующимкодом:

middle= min + (target — List(min)) * _

((max- min) / (List(max) — List(min)))

========270-271

@Рис.10.3 Интерполяционныйпоиск значения44

Этотоператор помещаетзначение middleмежду minи maxв таком жесоотношении, в каком искомоезначение находитсямежду List(min)и List(max).Если искомыйэлемент находитсярядом с List(min), то разностьtarget– List(min)почти равнанулю. Тогда всесоотношениецеликом выглядитпочти как middle= min+ 0, поэтомузначение переменнойmiddleпочти равноmin.Смысл этогозаключаетсяв том, что еслииндекс элементапочти равенmin, то его значениепочти равноList(min).
Аналогично, если искомыйэлемент находитсярядом с List(max), то разностьtarget– List(min)почти равнаразности List(max)– List(min).Их частноепочти равноединице, исоотношениевыглядит почтикак middle= min+ (max– min), или middle= max, если упроститьвыражение.Смысл этогосоотношениязаключаетсяв том, что еслизначение элементаблизко к List(max), то его индекспочти равенmax.
Послетого, как программавычислит значениеmiddle, она сравниваетзначение элементав этой позициис искомым также, как и в алгоритмедвоичногопоиска. Еслиэти значениясовпадают, тоискомый элементнайден и процессзакончен. Еслизначение искомогоэлемента меньше, чем значениенайденного, то программаустанавливаетзначение maxравным middle– 1 и продолжаетпоиск элементовсписка с меньшимизначениями.Если значениеискомого элементабольше, чемзначение найденного, то программаустанавливаетзначение minравным middle+ 1 и продолжаетпоиск элементовсписка с большимизначениями.
Заметьте, что в знаменателесоотношения, которое находитновое значениепеременнойmiddle, находитсяразность (List(max)– Lsit(min)).Если значенияList(max)и List(min)одинаковы, топроизойдетделение на нольи программааварийно завершитработу. Такоеможет произойти, если два элементав списке имеютодинаковыезначения. Таккак алгоритмподдерживаетсоотношениеmin
Чтобысправитьсяс этой проблемой, программа передвыполнениемоперации деленияпроверяет, неравны ли List(max)и List(min).Если это так, значит осталосьпроверитьтолько однозначение. Приэтом программапросто проверяет, совпадает лионо с искомым.
Ещеодна тонкостьзаключаетсяв том, что вычисленноезначение middleне всегда лежитмежду minи max.В простейшемслучае этоможет быть так, если значениеискомого элементавыходит запределы диапазоназначений элементовв списке. Предположим, что мы пытаемсянайти значение300 в списке изэлементов 100,150 и 200. На первомшаге вычисленийmin= 1 и max= 3. Тогдаmiddle= 1 + (300 – List(1)) * (3 – 1) / (List(3) –List(1)) = 1 + (300 – 100) * 2 / (200 – 100) = 5.Индекс 5 нетолько не находитсяв диапазонемежду minи max, он также выходитза границымассива. Еслипрограммапопытаетсяобратитьсяк элементумассива List(5), то она аварийнозавершит работус сообщениемоб ошибке “Subscriptoutofrange”.

===========272

Похожаяпроблема возникает, если значенияэлементовраспределенымежду minи maxочень неравномерно.Предположим, что мы хотимнайти значение100 в списке 0, 1, 2, 199,200. При первомвычислениизначения переменнойmiddle, мы получим впрограммеmiddle= 1 + (100 – 0) * (5 – 1) / (200 – 0) = 3.Затем программасравниваетзначение элементаList(3)с искомым значением100. Так как List(3)= 2, что меньше100, она задаетmin= middle+ 1, то естьmin= 4.
Приследующемвычислениязначения переменнойmiddle, программанаходит middle= 4 + (100 – 199) * (5 – 4) / (200 – 199) = -98.Значение –98 непопадает вдиапазон min
Еслирассмотретьпроцесс вычисленияпеременнойmiddle, то можно увидеть, что существуютдва варианта, при которыхновое значениеможет оказатьсяменьше, чем minили больше, чемmax.Вначале предположим, что middleменьше, чемmin.

min+ (target — List(min)) * ((max — min) / (List(max) — List(min)))

Послевычитания minиз обеих частейуравнения, получим:

(target- List(min)) * ((max — min) / (List(max) — List(min)))

Таккак max>= min, то разность(max– min)должна бытьбольше нуля.Так как List(max)>= List(min), то разность(List(max)– List(min))также должнабыть большенуля. Тогда всезначение можетбыть меньшенуля, толькоесли (target– List(min))меньше нуля.Это означает, что искомоезначение меньше, чем значениеэлемента List(min).В этом случае, искомый элементне может находитьсяв списке, таккак все элементысписка со значениемменьшим, чемList(min)уже были исключены.
Теперьпредположим, что middleбольше, чемmax.

min+ (target — List(min)) * ((max — min) / (List(max) — List(min))) >max

Послевычитания minиз обеих частейуравнения, получим:

(target- List(min)) * ((max — min) / (List(max) — List(min))) > 0

Умножениеобеих частейна (List(max)– List(min))/ (max– min)приводит соотношениек виду:

target– List(min) > List(max) – List(min)

И, наконец, прибавив кобеим частямList(min), получим:

target> List(max)

Этоозначает, чтоискомое значениебольше, чемзначение элементаList(max).В этом случае, искомое значениене может находитьсяв списке, таккак все элементысписка со значениямибольшими, чемList(max)уже были исключены.

==========273

Учитываявсе эти результаты, получаем, чтоновое значениепеременнойmiddleможет выйтииз диапазонамежду minи maxтолько в томслучае, еслиискомое значениевыходит запределы диапазонаот List(min)до List(max).Алгоритм можетиспользоватьэтот факт привычислениинового значенияпеременнойmiddle.Он вначалепроверяет, находится линовое значениемежду minи max.Если нет, тоискомого элементанет в спискеи работа алгоритмазавершена.
Следующийкод демонстрируетреализациюинтерполяционногопоиска в программеSearch:

PublicFunction InterpSearch(target As Long) As Long
Dimmin As Long
Dimmax As Long
Dimmiddle As Long

min= 1
max= NumItems
DoWhile min
'Избегаем деленияна ноль.
IfList(min) = List(max) Then
'Это искомыйэлемент (еслион есть в списке).
IfList(min) = target Then
InterpSearch= min
Else
InterpSearch= 0
EndIf
ExitFunction
EndIf

'Найти точкуразбиениясписка.
middle= min + (target — List(min)) * _
((max- min) / (List(max) — List(min)))

'Проверить, невышли ли мы заграницы.
Ifmiddle max Then
'Искомого элементанет в списке.
InterpSearch= 0
ExitFunction
EndIf

NumSearches= NumSearches + 1
Iftarget = List(middle) Then ' Искомыйэлементнайден.
InterpSearch= middle
ExitFunction
ElseIftarget
max= middle — 1
Else 'Поиск в правойчасти.
min= middle + 1
EndIf
Loop

'Если мы дошлидо этой точки, то элементанет в списке.
InterpSearch= 0
EndFunction

Двоичныйпоиск выполняетсяочень быстро, а интерполяционныйеще быстрее.В одном из тестов, двоичный поискпотребовалв 7 раз большевремени дляпоиска значенийв списке из100.000 элементов.Эта разницамогла бы бытьеще больше, если бы данныенаходилисьна диске иликаком либодругом медленномустройстве.Хотя при интерполяционномпоиске на вычисленияуходит большевремени, чемв случае двоичногопоиска, за счетменьшего числаобращений кдиску мы сэкономилибы гораздобольше времени.Строковыеданные
Еслиданные в спискепредставляютсобой строки, можно применитьдва различныхподхода. Болеепростой состоитв применениидвоичногопоиска. Придвоичном поискезначения элементовсравниваютсянепосредственно, поэтому этотметод можетлегко работатьсо строковымиданными.
С другойстороны, интерполяционныйпоиск используетчисленныезначения элементовданных длявычислениявозможногоположенияискомого элементав списке. Еслиэлементы представляютсобой строки, то этот алгоритмне может непосредственноиспользоватьзначения данныхдля вычисленияпредполагаемогоположенияискомого элемента.
Еслистроки достаточнокороткие, томожно закодироватьих при помощицелых чиселили чисел форматаlongили double, используяметоды, которыебыли описаныв 9 главе. Послеэтого можноиспользоватьдля нахожденияэлементов всписке интерполяционныйпоиск.
Еслистроки слишкомдлинные, и ихнельзя закодироватьдаже числамив формате double, то все еще можноиспользоватьдля интерполяциизначения строк.Вначале найдемпервый отличающийсясимвол длястрок List(min)и List(max).Затем закодируемего и следующиедва символав каждой строкепри помощиметодов из 9главы. Затемможно использоватьэти значениядля выполненияинтерполяционногопоиска.
Например, предположим, что мы ищемстроку TARGETв списке TABULATE,TANTRUM,TARGET,TATTERED,TAXATION.Если min= 1 и max= 5, то проверяютсязначения TABULATEи THEATER.Эти строкиотличаютсяво втором символе, поэтому нужнорассматриватьтри символа, начинающиесясо второго. Этобудут символыABUдля List(1),AXAдля List(5)и ARGдля искомойстроки.
Этизначения кодируютсячислами 804, 1378 и1222 соответственно.Подставляяэти значенияв формулу дляпеременнойmiddle, получим:

middle= min + (target — List(min)) * ((max — min) / (List(max) — List(min)))
=1 + (1222 – 804) * ((5 – 1) / (1378 – 804))
=2,91

=========275

Этопримерно равно3, поэтому следующеезначение переменнойmiddleравно 3. Этоположениестроки TARGETв списке, поэтомупоиск при этомзаканчивается.Следящийпоиск
Чтобыначать двоичныйследящий поиск(binary hunt andsearch), сравнимискомое значениеиз предыдущегопоиска с новымискомым значением.Если новоезначение меньше, начнем слежениевлево, еслибольше — вправо.
Длявыполненияслежения влево, установимзначения переменныхminи maxравными индексу, полученномуво время предыдущегопоиска. Затемуменьшим значениеminна единицу исравним искомоезначение созначениемэлемента List(min).Если искомоезначение меньше, чем значениеList(min), установим max= minи min= min–2, и сделаемеще одну проверку.Если искомоезначение всееще меньше, установим max= minи min= min–4, еслиэто не поможет, установим max= minи min= min–8 и такдалее. Продолжимустанавливатьзначение переменнойmaxравным значениюпеременнойminи вычитатьочередныестепени двойкииз значенияпеременнойminдо тех пор, покане найдетсязначение min, для которогозначение элементаList(min)будем меньшеискомого значения.
Необходимоследить за тем, чтобы не выйтиза границымассива, еслиminменьше, чемнижняя границамассива. Еслив какой томомент этоокажется так, то minнужно присвоитьзначение нижнейграницы массива.Если при этомзначение элементаList(min)все еще большеискомого, значитискомого элементанет в списке.На рис. 10.4 показанследящий поискэлемента созначением 17влево от предыдущегоискомого элементасо значением44.
Слежениевправо выполняетсяаналогично.Вначале значенияпеременныхminи maxустанавливаютсяравными значениюиндекса, полученногово время предыдущегопоиска. Затемпоследовательноустанавливаетсяmin= maxи max= max+ 1, min= maxи max= max+ 2, min= maxи max= max+ 4, и такдалее до техпор, пока в какой тоточке значениеэлемента массиваList(max)не станет большеискомого. Иснова необходимоследить за тем, чтобы не выйтиза границумассива.
Послезавершенияфазы слеженияизвестно, чтоиндекс искомогоэлемента находитсямежду minи max.После этогоможно использоватьобычный двоичныйпоиск для нахожденияточного положенияискомого элемента.

@Рис.10.4. Следящий поискзначения 17 иззначения 44

===============276

Еслиновый искомыйэлемент находитсянедалеко отпредыдущего, то алгоритмследящегопоиска оченьбыстро найдетзначения maxи min.Если новый истарый искомыеэлементы отстоятдруг от другана P позиций, то потребуетсяпорядка log(P)шагов для следящегопоиска новыхзначений переменныхminи max.
Предположим, что мы началиобычный двоичныйпоиск без фазыслежения. Тогдапотребуетсяпорядка log(NumItems)– log(P) шаговдля того, чтобызначения minи maxбыли на расстояниине больше, чемP позицийдруг от друга.Это означает, что следящийпоиск будетбыстрее обычногодвоичногопоиска, еслиlog(P) log(NumItems).Если возвестиобе части уравненияв степень двойки, получим 22*log(P)log(NumItems)или (2log(P))22
Из этогосоотношениявидно, что следящийпоиск будетвыполнятьсябыстрее, еслирасстояниемежду последовательнымиискомыми элементамибудет меньше, чем квадратныйкорень из числаэлементов всписке. Еслиследующие другза другом искомыеэлементы расположеныдалеко другот друга, толучше использоватьобычный двоичныйпоиск.Интерполяционныйследящий поиск
Используяметоды из предыдущихразделов можновыполнитьследящийинтерполяционныйпоиск (interpolativehunt andsearch). Вначале, как и раньше, сравним искомоезначение изпредыдущегопоиска с новым.Если новоеискомое значениеменьше, начнемслежение влево, если больше —вправо.
Дляслежения влевобудем теперьиспользоватьинтерполяцию, чтобы предположить, где может находитьсяискомое значениев диапазонемежду предыдущимзначением изначениемэлемента List(1).Но это будетпросто интерполяционныйпоиск, в которомmin= 1 и maxравно индексу, полученномуво время предыдущегопоиска. Послепервого шага, фаза слежениязаканчиваетсяи дальше можнопродолжитьобычный интерполяционныйпоиск.
Аналогичновыполняетсяслежение вправо.Просто приравниваемmax= Numitemsи устанавливаемminравным индексу, полученномуво время предыдущегопоиска. Затемпродолжаемобычный интерполяционныйпоиск.
На рис.10.5 показанинтерполяционныйпоиск элементасо значением17, начинающийсяс предыдущегоэлемента созначением 44.
Еслизначения данныхрасположеныпочти равномерно, то интерполяционныйпоиск всегдавыбирает значение, которое находитсярядом с искомымна первом илипоследующемшаге. Это означает, что начинаяс предыдущегонайденногозначения, нельзязначительноулучшить этоталгоритм. Напервом шаге, даже без использованиярезультатапредыдущегопоиска, интерполяционныйпоиск, вероятно, выберет индекс, который находитсядостаточноблизко от индексаискомого элемента.
log(NumItems)> min Or middle >    продолжение
--PAGE_BREAK--
@Рис.10.5. Интерполяционныйпоиск значения17 из значения44

=============277

С другойстороны, использованиепредыдущегозначения можетпомочь в случае, если данныераспределенынеравномерно.Если известно, что новое искомоезначение находитсяблизко к старому, интерполяционныйпоиск, начинающийсяс предыдущегозначения, обязательнонайдет элемент, который находитсярядом с предыдущимнайденным. Этоозначает, чтоиспользованиев качествестартовой точкипредыдущегонайденногозначения можетдавать определенноепреимущество.
Результатпредыдущегопоиска такжесильнее ограничиваетдиапазон возможныхположенийнового элемента, по сравнениюс диапазономот 1 до NumItems, поэтому алгоритмможет сэкономитьпри этом одинили два шага.Это особенноважно, еслисписок находитсяна диске иликаком либодругом медленномустройстве.Если сохранятьрезультатпредыдущегопоиска в памяти, то можно, покрайней мере, сравнить новоеискомое значениес предыдущимбез обращенияк диску.Резюме
Еслиэлементы находятсяв связном списке, используйтепоиск методомполного перебора.По возможностииспользуйтесигнальнуюметку в концесписка дляускоренияпоиска.
Есливам нужно времяот временипроводить поискв списке, содержащемдесятки элементов, также используйтепоиск методомполного перебора.Алгоритм в этомслучае будетпроще отлаживатьи поддерживать, чем более сложныеметоды поиска, и он будет даватьприемлемыерезультаты.
Еслитребуетсяпроводить поискв больших списках, используйтеинтерполяционныйпоиск. Еслизначения данныхраспределеныдостаточноравномерно, то интерполяционныйпоиск обеспечитнаилучшуюпроизводительность.Если списокнаходится надиске или каком либодругом медленномустройстве, разница в скоростимежду интерполяционнымпоиском и другимиметодами поискаможет бытьдостаточновелика.
Еслииспользуютсястроковыеданные, можнопопытатьсязакодироватьих числами вформате integer,longили double, при этом дляих поиска можнобудет использоватьинтерполяционныйметод. Еслистроки слишкомдлинные и непомещаютсядаже в числаформата double, то проще всегоможет оказатьсяиспользоватьдвоичный поиск.В табл. 10.1 перечисленыпреимуществаи недостаткидля различныхметодов поиска.
Используядвоичный илиинтерполяционныйпоиск, можноочень быстронаходить элементыдаже в оченьбольших списках.Если значенияданных распределеныравномерно, то интерполяционныйпоиск позволяетвсего за несколькошагов найтиэлемент в списке, содержащеммиллион элементов.

@Таблица10.1 Преимуществаи недостаткиразличныхметодов поиска.

===========278

Тем неменее, в такойбольшой списоктрудно вноситьизменения.Вставка илиудаление элементаиз упорядоченногосписка займетвремя порядкаO(N). Еслиэлемент находитсяв начале списка, выполнениеэтих операцийможет потребоватьочень большогоколичествавремени, особенноесли списокнаходится накаком либомедленномустройстве.
Еслитребуетсявставлять иудалять элементыиз большогосписка, следуетрассмотретьвозможностьзамены его надругую структуруданных. В 7 главеобсуждаютсясбалансированныедеревья, вставкаи добавлениеэлемента вкоторые требуетвремени порядкаO(log(N)).
В 11 главеобсуждаютсяметоды, позволяющиевыполнятьвставку и удалениеэлементов ещебыстрее. Длядостижениятакой высокойскорости, вэтих методахиспользуетсядополнительноепространстводля храненияпромежуточныхданных. Хеш таблицыне хранят информациюо порядкерасположенияданных. В хеш таблицуможно вставлять, удалять, и находитьэлементы, носложно вывестиэлементы изтаблицы попорядку.
Еслисписок будетнеизменным, то применениеупорядоченногосписка и использованиеметода интерполяционногопоиска дастпрекрасныерезультаты.Если требуетсячасто вставлятьи удалять элементыиз списка, тостоит рассмотретьвозможностьпримененияхеш таблицы.Если при этомтакже нужновыводить элементыпо порядку илиперемещатьсяпо списку впрямом илиобратном направлении, то оптимальнуюскорость игибкость можетобеспечитьприменениесбалансированныхдеревьев. Решив, какие типаопераций вампонадобятся, вы можете выбратьалгоритм, которыйвам лучше всегоподходит.

=============279
Глава11. Хеширование
В предыдущейглаве описывалсяалгоритминтерполяционногопоиска, которыйиспользуетинтерполяцию, чтобы быстронайти элементв списке. Сравниваяискомое значениесо значениямиэлементов визвестныхточках, этоталгоритм можетопределитьвероятноеположениеискомого элемента.В сущности, онсоздает функцию, которая устанавливаетсоответствиемежду искомымзначением ииндексом позиции, в которой ондолжен находиться.Если первоепредположениеошибочно, тоалгоритм сноваиспользуетэту функцию, делая новоепредположение, и так далее, дотех пор, покаискомый элементне будет найден.
Хеширование(hashing) используетаналогичныйподход, отображаяэлементы вхеш таблице(hash table).Алгоритм хешированияиспользуетнекоторуюфункцию, котораяопределяетвероятноеположениеэлемента втаблице наоснове значенияискомого элемента.
Например, предположим, что требуетсязапомнитьнесколькозаписей, каждаяиз которыхимеет уникальныйключ со значениемот 1 до 100. Для этогоможно создатьмассив со 100ячейками ипроинициализироватькаждую ячейкунулевым ключом.Чтобы добавитьв массив новуюзапись, данныеиз нее простокопируютсяв соответствующуюячейку массива.Чтобы добавитьзапись с ключом37, данные из неепросто копируютсяв 37 позицию вмассиве. Чтобынайти записьс определеннымключом, простовыбираетсясоответствующаяячейка массива.Для удалениязаписи ключусоответствующейячейки массивапросто присваиваетсянулевое значение.Используя этусхему, можнодобавить, найтии удалить элементиз массива заодин шаг.
К сожалению, в реальныхприложенияхзначения ключане всегда находятсяв небольшомдиапазоне.Обычно диапазонвозможныхзначений ключадостаточновелик. Базаданных сотрудниковможет использоватьв качествеключа идентификационныйномер социальногострахования.Теоретическиможно было бысоздать массив, каждая ячейкакоторогосоответствовалаодному из возможныхдевятизначныхчисел; но напрактике дляэтого не хватитпамяти илидисковогопространства.Если для храненияодной записитребуется 1килобайт памяти, то такой массивзанял бы 1 терабайт(миллион мегабайт)памяти. Дажеесли можно былобы выделитьтакой объемпамяти, такаясхема была быочень неэкономной.Если штат вашейкомпании меньше10 миллионовсотрудников, то более 99 процентовмассива будутпусты.

=======281

Чтобысправитьсяс этой проблемой, схемы хешированияотображаютпотенциальнобольшое числовозможныхключей на достаточнокомпактнуюхеш таблицу.Если в вашейкомпании работает700 сотрудников, вы можете создатьхеш таблицус 1000 ячеек. Схемахешированияустанавливаетсоответствиемежду 700 записямио сотрудникахи 1000 позициямив таблице. Например, можно располагатьзаписи в таблицев соответствиис тремя первымицифрами идентификационногономера в системесоциальногострахования.При этом записьо сотрудникес номером социальногострахования123 45 6789 будет находитьсяв 123 ячейке таблицы.
Очевидно, что посколькусуществуетбольше возможныхзначений ключа, чем ячеек втаблице, тонекоторыезначения ключеймогут соответствоватьодним и тем жеячейкам таблицы.Например, обазначения 123 45 6789и 123­99 9999 отображаютсяна одну и ту жеячейку таблицы123. Если существуетмиллиард возможныхномеров системысоциальногострахования, и таблица имеет1000 ячеек, то всреднем каждаяячейка будетсоответствоватьмиллиону записей.
Чтобыизбежать этойпотенциальнойпроблемы, схемахешированиядолжна включатьв себя алгоритмразрешенияконфликтов(collision resolutionpolicy), которыйопределяетпоследовательностьдействий вслучае, еслиключ соответствуетпозиции в таблице, которая ужезанята другойзаписью. В следующихразделах описываютсянесколькоразличныхметодов разрешенияконфликтов.
Всеобсуждаемыездесь методыиспользуютдля разрешенияконфликтовпримерно одинаковыйподход. Онивначале устанавливаютсоответствиемежду ключомзаписи и положениемв хеш таблице.Если эта ячейкауже занята, ониотображаютключ на какую либодругую ячейкутаблицы. Еслиона также ужезанята, то процессповторяетсяснова о техпор, пока в концеконцов алгоритмне найдет пустуюячейку в таблице.Последовательностьпроверяемыхпри поиске иливставке элементав хеш таблицупозиций называетсятестовойпоследовательностью(probe sequence).
В итоге, для реализациихешированиянеобходимытри вещи:
Структура данных (хеш таблица) для хранения данных;
Функция хеширования, устанавливающая соответствие между значением ключа и положением в таблице;
Алгоритм разрешения конфликтов, определяющий последовательность действий, если несколько ключей соответствуют одной ячейке таблицы.
В следующихразделах описанынекоторыеструктурыданных, которыеможно использоватьдля хеширования.Каждая из нихимеет соответствующуюфункцию хешированияи один или болееалгоритмовразрешенияконфликтов.Так же, как и вбольшинствекомпьютерныхалгоритмов, каждый из этихметодов имеетсвои преимуществаи недостатки.В последнемразделе описаныпреимуществаи недостаткиразных методов, чтобы помочьвам выбратьнаилучший дляданной ситуацииметод хеширования.Связывание
Одиниз методовразрешенияконфликтовзаключаетсяв хранениизаписей, которыезанимают одинаковоеположение втаблице, в связныхсписках. Чтобыдобавить втаблицу новуюзапись, припомощи функциихешированиявыбираетсясвязный список, который долженего содержать.Затем записьдобавляетсяв этот список.
На рис.11.1 показан примерсвязыванияхеш таблицы, которая содержит10 ячеек. Функцияхешированияотображаетключ K наячейку KMod 10 в массиве.Каждая ячейкамассива содержитуказатель напервый элементсвязного списка.При вставкеэлемента втаблицу онпомещаетсяв соответствующийсписок.

======282

@Рис.11.1. Связывание

Чтобысоздать хеш таблицув Visual Basic, используйтеоператор ReDimдля размещениясигнальныхметок началасписков. Есливы хотите создатьв хеш таблицеNumListsсвязных списков, задайте размермассива ListTopsпри помощиоператора ReDimListTops(0ToNumLists- 1). Первоначальновсе спискипусты, поэтомууказательNextCellкаждой меткидолжен иметьзначение Nothing.Если вы используетедля изменениямассива метокоператор ReDim, то Visual BasicавтоматическиинициализируетуказателиNextCellзначениемNothing.
Чтобынайти в хеш таблицеэлемент с ключомK, нужно вычислитьKModNumLists, получив индексметки связногосписка, которыйможет содержатьискомый элемент.Затем нужнопросмотретьсписок до техпор, пока искомыйэлемент небудет найденили процедуране дойдет доконца списка.

GlobalConst HASH_FOUND = 0
GlobalConst HASH_NOT_FOUND = 1
GlobalConst HASH_INSERTED = 2

PrivateFunction LocateItemUnsorted(Value As Long) As Integer
Dimcell As ChainCell

'Получить вершинусвязного списка.
Setcell = m_ListTops(Value Mod NumLists).NextCell
DoWhile Not (cell Is Nothing)
Ifcell.Value = Value Then Exit Do
Setcell = cell.NextCell
Loop
Ifcell Is Nothing Then
LocateItemUnsorted= HASH_NOT_FOUND
Else
LocateItemUnsorted= HASH_FOUND
EndIf
EndFunction

Функциидля вставкии удаленияэлементов изсвязных спискованалогичныфункциям, описаннымво 2 главе.

========283
Преимуществаи недостаткисвязывания
Одноиз преимуществэтого методасостоит в том, что при егоиспользованиихеш таблицыникогда непереполняются.При этом вставкаи поиск элементоввсегда выполняетсяочень просто, даже если элементовв таблице оченьмного. Для некоторыхметодов хеширования, описанных ниже, производительностьзначительнопадает, еслитаблица почтизаполнена.
Изхеш таблицы, которая используетсвязывание, также простоудалять элементы, при этом элементпросто удаляетсяиз соответствующегосвязного списка.В некоторыхдругих схемаххешированияудалить элементнепросто илиневозможно.
Одиниз недостатковсвязываниясостоит в том, что если числосвязных списковнедостаточновелико, то размерсписков можетстать большим, при этом длявставки илипоиска элементанеобходимобудет проверитьбольшое числоэлементовсписка. Еслихеш таблицасодержит 10 связныхсписков и к нейдобавляется1000 элементов, то средняядлина связногосписка будетравна 100. Чтобынайти элементв таблице, придетсяпроверитьпорядка 100 ячеек.
Можнонемного ускоритьпоиск, еслииспользоватьупорядоченныесписки. Тогдаможно использоватьдля поискаэлементов вупорядоченныхсвязных спискахметоды, описанныев 10 главе. Этопозволяетпрекратитьпоиск, если вовремя его выполнениявстретитсяэлемент созначением, большим искомого.В среднем потребуетсяпроверитьтолько половинусвязного списка, чтобы найтиэлемент илиопределить, что его нет всписке.

PrivateFunction LocateItemSorted(Value As Long) As Integer
Dimcell As ChainCell

'Получить вершинусвязного списка.
Setcell = m_ListTops(Value Mod NumLists).NextCell
DoWhile Not (cell Is Nothing)
Ifcell.Value >= Value Then Exit Do
Setcell = cell.NextCell
Loop

Ifcell Is Nothing Then
LocateItemSorted= HASH_NOT_FOUND
ElseIfcell.Value = Value Then
LocateItemSorted= HASH_FOUND
Else
LocateItemSorted= HASH_NOT_FOUND
EndIf
EndFunction

Использованиеупорядоченныхсписков позволяетускорить поиск, но не снимаетнастоящуюпроблему, связаннуюс переполнениятаблицы. Лучшим, но более трудоемкимрешением будетсоздание хеш таблицыбольшего размераи повторноехешированиеэлементов вновой таблицетак, чтобы связныесписки в нейимели меньшийразмер. Этоможет занятьдовольно многовремени, особенноесли спискизаписаны надиске или каком либодругом медленномустройстве, а не в памяти.

========284

В программеChainреализованахеш таблицасо связыванием.Введите числосписков в полеобласти TableCreation (Созданиетаблицы) наформе и установитефлажок SortLists (Упорядоченныесписки), есливы хотите, чтобыпрограммаиспользовалаупорядоченныесписки. Затемнажмите накнопку CreateTable (Создатьтаблицу). Затемвы можете ввестиновые значенияи снова нажатьна кнопку CreateTable, чтобысоздать новуюхеш таблицу.
Таккак интересноизучать хеш таблицы, содержащиебольшое числозначений, топрограмма Chainпозволяетзаполнятьтаблицу случайнымиэлементами.Введите числоэлементов, которые выхотите создатьи максимальноезначение элементовв области RandomItems (Случайныеэлементы), затемнажмите накнопку CreateItems (Создатьэлементы), ипрограммадобавит в хеш таблицуслучайно созданныеэлементы.
И, наконец, введите значениев области Search(Поиск). Есливы нажмете накнопку Add(Добавить), топрограммавставит элементв хеш таблицу, если он еще ненаходится вней. Если вынажмете накнопку Find(Найти), то программавыполнит поискэлемента втаблице.
Послезавершенияоперации поискаили вставки, программавыводит статусоперации внижней частиформы — былали операцияуспешной ичисло проверенныхво время еевыполненияэлементов.
В строкестатуса такжевыводитсясредняя длинауспешной (еслиэлемент естьв таблице) ибезуспешной(если элементав таблице нет)тестовыхпоследовательностей.Программавычисляет этизначения, выполняяпоиск для всехчисел междуединицей инаибольшимчислом в хеш таблице, и затем подсчитываясреднее значениедлины тестовойпоследовательности.
На рис.11.2 показано окнопрограммы Chainпосле успешногопоиска элемента414.Блоки
Другойспособ разрешенияконфликтовзаключаетсяв том, чтобывыделить рядблоков, каждыйиз которыхможет содержатьнесколькоэлементов. Длявставки элементав таблицу, онотображаетсяна один из блокови затем помещаетсяв этот блок.Если блок ужезаполнен, тоиспользуетсяобработкапереполнения.

@Рис.11.2. ПрограммаChain


======285

Возможно, самый простойметод обработкипереполнениясостоит в том, чтобы поместитьвсе лишниеэлементы вспециальныеблоки в концемассива «нормальных»блоков. Этопозволяет принеобходимостилегко увеличиватьразмер хеш таблицы.Если требуетсябольше дополнительныхблоков, то размермассива блоковпросто увеличивается, и в конце массивасоздаются новыедополнительныеблоки.
Например, чтобы добавитьновый элементK в хеш таблицу, которая содержитпять блоков, вначале мыпытаемся поместитьего в блок сномером KMod 5. Если этотблок заполнен, элемент помещаетсяв дополнительныйблок.
Чтобынайти элементв таблице, вычислимK Mod 5, чтобынайти его положение, и затем выполнимпоиск в этомблоке. Еслиэлемента в этомблоке нет, иблок не заполнен, значит элементав хеш таблиценет. Если элементав блоке нет иблок заполнен, необходимопроверитьдополнительныеблоки.
На рис.11.3 показаны пятьблоков с номерамиот 0 до 4 и одиндополнительныйблок. Каждыйблок можетсодержать по5 элементов. Вэтом примерев хеш таблицубыли вставленыследующиеэлементы: 50, 13, 10,72, 25, 46, 68, 30, 99, 85, 93, 65, 70. Привставке элементов65 и 70 блоки ужебыли заполнены, поэтому этиэлементы былипомещены впервый дополнительныйблок.
Чтобыреализоватьметод блочногохешированияв Visual Basic, можно использоватьдля храненияблоков двумерныймассив. ЕслитребуетсяNumBucketsблоков, каждыйиз которыхможет содержатьBucketSizeячеек, выделимпамять подблоки при помощиоператора ReDimTheBuckets(0ToBucketSize-1, 0 ToNumBuckets- 1). Второеизмерениесоответствуетномеру блока.Оператор VisualBasic ReDimпозволяетизменить толькоразмер массива, поэтому номерблока долженбыть вторымизмерениеммассива.
Чтобынайти элементK, вычислим номерблока KModNumBuckets.Затем проведемпоиск в блокедо тех пор, покане найдетсяискомый элемент, или пустаяячейка блока, или блок незакончится.Если элементнайден, поискзавершен. Есливстретитсяпустая ячейка, значит элементав хеш таблиценет, и процесстакже завершен.Если проверенвесь блок, и ненайден искомыйэлемент илипустая ячейка, требуетсяпроверитьдополнительныеблоки.
    продолжение
--PAGE_BREAK--
@Рис.11.3. Хешированиес использованиемблоков

======286

PublicFunction LocateItem(Value As Long, _
bucket_probesAs Integer, item_probes As Integer) As Integer
Dimbucket As Integer
Dimpos As Integer

bucket_probes= 1
item_probes= 0

'Определить, к какому блокуон относится.
bucket= (Value Mod NumBuckets)
'Поиск элементаили пустойячейки.
Forpos = 0 To BucketSize — 1
item_probes= item_probes + 1
IfBuckets(pos, bucket).Value = UNUSED Then
LocateItem= HASH_NOT_FOUND 'Элементотсутствует.
ExitFunction
EndIf
IfBuckets(pos, bucket).Value = Value Then
LocateItem= HASH_FOUND 'Элементнайден.
ExitFunction
EndIf
Nextpos

'Проверитьдополнительныеблоки.
Forbucket = NumBuckets To MaxOverflow
bucket_probes= bucket_probes + 1
Forpos = 0 To BucketSize — 1
item_probes= item_probes + 1
IfBuckets(pos, bucket).Value = UNUSED Then
LocateItem= HASH_NOT_FOUND ' Not here.
ExitFunction
EndIf
IfBuckets(pos, bucket).Value = Value Then
LocateItem= HASH_FOUND 'Элементнайден.
ExitFunction
EndIf
Nextpos
Nextbucket

'Если элементдо сих пор ненайден, то егонет в таблице.
LocateItem= HASH_NOT_FOUND
EndFunction

======287

ПрограммаBucketдемонстрируетэтот метод. Этапрограмма оченьпохожа на программуChain, но она используетблоки, а не связныесписки. Когдаэта программавыводит длинутестовойпоследовательности, она показываетчисло проверенныхблоков и числопроверенныхэлементов вблоках. На рис.11.4 показано окнопрограммы послеуспешногопоиска элемента661 в первом дополнительномблоке. В этомпримере программапроверила 9элементов вдвух блоках.Хранениехеш таблицна диске
Многиезапоминающиеустройства, такие как стримеры, дисководы ижесткие диски, могут считыватьбольшие кускиданных за однообращение кустройству.Обычно этиблоки имеютразмер 512 или1024 байта. Чтениевсего блокаданных занимаетстолько жевремени, сколькои чтение одногобайта.
Еслиимеется большаяхеш таблица, записаннаяна диске, тоэтот факт можноиспользоватьдля улучшенияпроизводительности.Доступ к даннымна диске занимаетнамного большевремени, чемдоступ к даннымв памяти. Еслисразу загружатьвсе элементыблока, то можнобудет прочитатьих все во времяодного обращенияк диску. Послетого, как всеэлементы окажутсяв памяти, ихпроверка можетвыполнятьсянамного быстрее, чем если быпришлось ихсчитывать сдиска по одному.
Еслидля чтенияэлементов сдиска используетсяцикл For, то Visual Basicбудет обращатьсяк диску причтении каждогоэлемента. Сдругой стороны, можно использоватьоператор VisualBasic Getдля чтениявсего блокасразу. При этомпотребуетсявсего однообращение кдиску, и программабудет выполнятьсянамного быстрее.
Можносоздать типданных, которыйбудет содержатьмассив элементов, представляющийблок. Так какво время работыпрограммынельзя изменятьразмер массивав определенномпользователемтипе, то необходимозаранее определить, сколько элементовсможет находитьсяв блоке. Приэтом возможностиизмененияразмеров блоковограниченыпо сравнениюс предыдущимвариантомалгоритма.

GlobalConst ITEMS_PER_BUCKET = 10 'Числоэлементовв блоке.
GlobalConst MAX_ITEM = 9 'ITEMS_PER_BUCKET — 1.

TypeItemType
ValueAs Long
EndType
GlobalConst ITEM_SIZE = 4 ' Размерданныхэтоготипа.

TypeBucketType
Item(0To MAX_ITEM) As ItemType
EndType
GlobalConst BUCKET_SIZE = ITEMS_PER_BUCKET * ITEM_SIZE

Передтем, как начатьчтение данныхиз файла, оноткрываетсядля произвольногодоступа:

Openfilename For Random As #DataFile Len = BUCKET_SIZE

=========288

@Рис.11.4. ПрограммаBucket

Дляудобства работыможно написатьфункции длячтения и записиблоков. Этифункции читаюти пишут данныев глобальнуюпеременнуюTheBucket, которая содержитданные одногоблока. Послетого, как данныезагружены вэту переменную, можно выполнитьпоиск средиэлементов этогоблока в памяти.
Таккак при произвольномобращении кфайлу записинумеруютсяс единицы, а нес нуля, то этифункции должныдобавлять кномеру блокав хеш таблицеединицу передсчитываниемданных из файла.Например, нулевомублоку в хеш таблицебудет соответствоватьзапись с номером1.

PrivateSub GetBucket(num As Integer)
Get#DataFile, num + 1, TheBucket
EndSub

PrivateSub PutBucket(num As Integer)
Put#DataFile, num + 1, TheBucket
EndSub

Используяфункции GetBucketи PutBucket, можно переписатьпроцедуру поискв хеш таблицедля чтениязаписей изфайла:

PublicFunction LocateItem(Value As Long, _
bucket_probesAs Integer, item_probes As Integer) As Integer
Dimbucket As Integer
Dimpos As Integer

item_probes= 0

'Определить, к какому блокупринадлежитэлемент.
GetBucketValue Mod NumBuckets
bucket_probes= 1

'Поиск элементаили пустойячейки.
Forpos = 0 To MAX_ITEM
item_probes= item_probes + 1
IfTheBucket.Item(pos).Value = UNUSED Then
LocateItem= HASH_NOT_FOUND ' Элементанетв таблице.
ExitFunction
EndIf
IfTheBucket.Item(pos).Value = Value Then
LocateItem= HASH_FOUND ' Элементнайден.
ExitFunction
EndIf
Nextpos
'Проверитьдополнительныеблоки
Forbucket = NumBuckets To MaxOverflow
'Проверитьследующийдополнительныйблок.
GetBucketbucket
bucket_probes= bucket_probes + 1
Forpos = 0 To MAX_ITEM
item_probes= item_probes + 1
IfTheBucket.Item(pos).Value = UNUSED Then
LocateItem= HASH_NOT_FOUND 'Элементанет.
ExitFunction
EndIf
IfTheBucket.Item(pos).Value = Value Then
LocateItem= HASH_FOUND 'Элементнайден.
ExitFunction
EndIf
Nextpos
Nextbucket
'Если элементвсе еще не найден, его нет в таблице.
LocateItem= HASH_NOT_FOUND
EndFunction

ПрограммаBucket2аналогичнапрограммеBucket, но она хранитблоки на диске.Она также невычисляет ине выводит наэкран среднююдлину тестовойпоследовательности, так как этивычисленияпотребовалибы большогочисла обращенийк диску и сильнозамедлили быработу программы.

============290

Таккак при обращениик блокам происходитчтение с диска, а обращениек элементамблока происходитв памяти, точисло проверяемыхблоков гораздосильнее влияетна время выполненияпрограммы, чемполное числопроверенныхэлементов. Длясравнениясреднего числапроверенныхблоков и элементовпри поискеэлементов можноиспользоватьпрограммуBucket.
Каждыйблок в программеBucket2может содержатьдо 10 элементов.Это позволяетлегко вставлятьэлементы вблоки до техпор, пока онине переполнятся.В реальнойпрограммеследует попытатьсяпоместить вблок максимальновозможное числоэлементов так, чтобы размерблока оставалсяпри этом равнымцелому числукластеровдиска.
Например, можно читатьданные блокамипо 1024 байта. Еслиэлемент данныхимеет размер44 байта, то в одинблок можетпоместиться23 элемента данных, и при этом размерблока будетменьше 1024 байт.

GlobalConst ITEMS_PER_BUCKET = 23 'Числоэлементовв блоке.
GlobalConst MAX_ITEM = 22 'ITEMS_PER_BUCKET — 1.

TypeItemType
LastNameAs String * 20 ' 20 байт.
FirstNameAs String * 20 ' 20 байт.
EmloyeeIdAs Long ' 4 байта(это ключ).
EndType
GlobalConst ITEM_SIZE = 44 Размерданныхэтоготипа.

TypeBucketType
Item(0To MAX_ITEM) As ItemType
EndType
GlobalConst BUCKET_SIZE = ITEMS_PER_BUCKET * ITEM_SIZE

Размещениев каждом блокебольшего числаэлементовпозволяетсчитыватьбольше данныхпри каждомобращении кдиску. При этомв таблице такжеможет бытьбольше элементов, прежде чембудет необходимоиспользоватьдополнительныеблоки. Доступк дополнительнымблокам требуетдополнительныхобращений кдиску, поэтомуследует повозможностиизбегать его.
С другойстороны, еслиблоки достаточновелики, то онимогут содержатьбольшое числопустых ячеек.Если данныенеравномернораспределеныпо блокам, тоодни блокимогут бытьпереполнены, а другие —практическипусты. Использованиедругого вариантаразмещенияс большим числомблоков меньшегоразмера можетуменьшить этупроблему. Дажеесли некоторыеблоки все ещебудут переполнены, а некоторыепусты, то почтипустые блокибудут иметьменьший размер, потому они небудут содержатьтак много пустыхячеек.
На рис.11.5 показаны дваварианта расположенияодних и тех жеданных в блоках.В расположениинаверху используются5 блоков, каждыйиз которыхсодержит по5 элементов.При этом дополнительныеблоки не используются, и всего имеется12 пустых ячеек.Расположениевнизу использует10 блоков, каждыйиз которыхсодержит по2 элемента. Внем имеется9 пустых ячееки один дополнительныйблок.

========291

@Рис.11.5. Два вариантарасположенияэлементов вблоках

Этопример пространственно временногокомпромисса.При первомрасположениивсе элементырасположеныв обычных (недополнительных)блоках, поэтомуможно быстронайти любойиз них. Второерасположениезанимает меньшеместа, но помещаетнекоторыеэлементы вдополнительныеблоки, при этомдоступ к нимзанимает большевремени.Связываниеблоков
Можноиспользоватьдругой подход, если при переполненииблоков создаватьцепочки изблоков. Длякаждого заполненногоблока создаетсясвоя цепочкаблоков, вместотого, чтобыхранить вселишние элементыв одних и техже дополнительныхблоках. Припоиске элементав заполненномблоке нетнеобходимостипроверятьэлементы вдополнительныхблоках, которыебыли помещенытуда в результатепереполнениядругих блоков.Если множествоблоков переполнено, то это можетсэкономитьдовольно многовремени.
На рис.11.6 показаноприменениедвух разныхсхем хешированиядля одних и техже данных. Вверхулишние элементыпомещаютсяв общие дополнительныеблоки. Чтобынайти элементы32 и 30, нужно проверитьтри блока. Во первых, проверяетсяблок, в которомэлемент долженнаходится.Элемента в этомблоке нет, поэтомупроверяетсяпервый дополнительныйблок, в которомэлемента тоженет. Поэтомутребуетсяпроверитьвторой дополнительныйблок, в котором, наконец, находитсяискомый элемент.
В нижнемрасположениизаполненныеблоки связанысо своимисобственнымидополнительнымиблоками. Притаком расположениилюбой элементможно найтипосле обращенияне более чемк двум блокам.Как и раньше, вначале проверяетсяблок, в которомэлемент долженнаходиться.Если его тамнет, то проверяетсясвязный списокдополнительныхблоков. В этомпримере чтобынайти искомыйэлемент нужнопроверитьтолько одиндополнительныйблок.

=========292

@Рис.11.6. Связныедополнительныеблоки

Еслидополнительныеблоки хеш таблицысодержит большоечисло элементов, то организацияцепочек издополнительныхблоков можетсэкономитьдостаточномного времени.Предположим, что имеетсяотносительнобольшая хеш таблица, содержащая1000 блоков, в каждомиз которыхнаходится 10элементов.Предположимтакже, что вдополнительныхблоках находится1000 элементов, для которыхпонадобится100 дополнительныхблоков. Чтобынайти один изпоследнихэлементов вдополнительныхблоках, потребуетсяпроверить 101блок.
Болеетого, предположим, что мы пыталисьнайти элементK, которогонет в таблице, но которыйдолжен был бынаходитьсяв одном иззаполненныхблоков. В этомслучае пришлосьбы проверитьвсе 100 дополнительныхблоков, преждечем выяснилосьбы, что элементотсутствуетв таблице. Еслипрограмма частопытается найтиэлементы, которыхнет в таблице, то значительнаячасть временибудет тратитьсяна проверкудополнительныхблоков.
Еслидополнительныеблоки связанымежду собойи ключевыезначения распределеныравномерно, то можно будетнаходить элементынамного быстрее.Если максимальноечисло дополнительныхэлементов дляодного блокаравно 10, то каждыйблок можетиметь не большеодного дополнительного.В этом случаеможно найтиэлемент илиопределить, что его нет втаблице, проверивне более двухблоков.
С другойстороны, еслихеш таблицатолько слегкапереполнена, то многие блокибудут иметьдополнительныеблоки, содержащиевсего один илидва элемента.Допустим, чтов каждом блокедолжно находиться11 элементов.Так как каждыйблок можетвместить только10 элементов, для каждогообычного блоканужно будетсоздать одиндополнительный.В этом случаепотребуется1000 дополнительныхблоков, каждыйиз которыхбудет содержатьвсего одинэлемент, и всегов дополнительныхблоках будет900 пустых ячеек.
Этоеще один примерпространственно временногокомпромисса.Связываниеблоков другс другом позволяетбыстрее вставлятьи находитьэлементы, нооно также можетзаполнятьхеш таблицупустыми ячейками.Конечно, можноизбежать этойпроблемы, создавновую хеш таблицубольшего размераи разместивв ней все элементытаблицы.

=====293
Удалениеэлементов
Удалениеэлементов изблоков сложнее, чем из связныхсписков, но оновозможно. Во первых, найдем элемент, который требуетсяудалить изхеш таблицы.Если блок незаполнен, тона место удаленногоэлемента помещаетсяпоследнийэлемент блока, при этом всенепустые ячейкиблока будетнаходитьсяв его начале.Тогда, если припоиске элементав блоке позднеенайдется пустаяячейка, то можнобудет заключить, что элементав таблице нет.
Еслиблок, содержащийискомый элемент, заполнен, тонужно провестипоиск заменяющегоего элементав дополнительныхблоках. Еслини один из элементовв дополнительныхблоках не принадлежитк данному блоку, то искомыйэлемент заменяетсяпоследнимэлементом вблоке, и последняяячейка блокастановитсяпустой.
Иначе, если в дополнительномблоке существуетэлемент, которыйпринадлежитк данному блоку, то найденныйэлемент издополнительногоблока помещаетсяна место удаленногоэлемента. Приэтом в дополнительномблоке образуетсяпустое пространство, но это легкоисправить —в образовавшуюсяпустую ячейкупомещаетсяпоследнийэлемент изпоследнегодополнительногоблока.
На рис.11.7 показан процессудаления элементаиз заполненногоблока. Во первых, из блока 0 удаляетсяэлемент 24. Таккак блок 0 былзаполнен, тонужно попытатьсянайти элементиз дополнительныхблоков, которыйможно было бывставить наего место вблок 0. В данномслучае блок0 содержит всечетные элементы, поэтому любойчетный элементиз дополнительныхблоков подойдет.Первый четнымэлементом вдополнительныхблоках будетэлемент 14, поэтомуможно заменитьэлементы 24 вблоке 0 элементом14.
Приэтом в третьейпозиции первогодополнительногоблока образуетсяпустая ячейка.Заполним еепоследнимэлементом изпоследнегодополнительногоблока, в данномслучае элементом79. В этот моментхеш таблицаснова готовак работе.
Другойметод состоитв том, чтобывместо удаленияэлемента помечатьего как удаленный.Для поискаэлементов втаком блокенужно игнорироватьудаленныеэлементы. Еслипозднее в блокбудут добавлятьсяновые элементы, можно будетпомещать ихна место элементов, помеченныхкак удаленные.

@Рис.11.7. Удалениеэлемента изблока

=========294

Быстрееи легче вместоудаления элементапросто помечатьего как удаленный, но, в конце концов, таблица можетоказатьсязаполненнойнеиспользуемымиячейками. Еслидобавить вхеш таблицуряд элементови затем удалитьбольшинствоиз них в порядкепервый вошел —первый вышел, то расположениеэлементов вблоках можетоказаться«перевернутым».Большая частьнастоящихданных будетнаходитьсяв конце блокови в дополнительныхблоках. Добавлятьновые элементыв таблицу будетпросто, но припоиске элементадовольно многовремени будеттратиться напропуск удаленныхэлементов.
В качествекомпромиссапри удаленииэлемента изблока можноперемещатьпоследнийэлемент блокана освободившеесяместо и затемпомечать последнийэлемент блокакак удаленный.Тогда при поискев блоке можнопрекратитьдальнейшийпоиск в блоке, если при этомвстретитсяэлемент, помеченный, как удаленный.После этогоможно провестипоиск в дополнительныхблоках, еслиони существуют.Преимуществаи недостаткипримененияблоков
Вставкаи удалениеэлемента вхеш таблицус блоками выполняетсядостаточнобыстро, дажеесли таблицапочти заполнена.Фактически, хеш таблица, использующаяблоки, обычнобудет быстрее, чем таблицасо связыванием(связываниемиз предыдущейглавы, а несвязываниемблоков). Еслихеш таблицанаходится надиске, блочныйалгоритм можетсчитывать заодно обращениек диску весьблок. При использованиисвязных списков, следующийэлемент можетнаходитьсяна диске необязательнорядом с предыдущим.При этом длякаждой проверкиэлемента потребуетсяобращение кдиску.
Удалениеэлемента изтаблицы сложнеевыполнить сиспользованиемблоков, чем приприменениисвязных списков.Чтобы удалитьэлемент иззаполненногоблока, можетпонадобитьсяпроверить вседополнительныеблоки в поискеэлемента, которыйнужно поместитьна его место.
И ещеодно преимуществохеш таблицы, использующейблоки, состоитв том, что еслитаблица переполняется, то можно легкоувеличить ееразмер. Когдавсе дополнительныеблоки заполнятся, можно простоизменить размермассива и создатьв его конценовый дополнительныйблок.
Еслимногократноувеличиватьразмер таблицыподобным образом, то большаячасть данныхможет находитьсяв дополнительныхблоках. Тогдадля того, чтобынайти или вставитьэлемент, потребуетсяпроверитьмножествоблоков, ипроизводительностьупадет. В этомслучае, можетбыть лучшесоздать новуюхеш таблицус большим числомосновных блокови поместитьэлементы в нее.Открытаяадресация
Иногдаэлементы данныхслишком велики, чтобы их былоудобно размещатьв блоках. Еслитребуетсясписок из 1000элементов, каждый из которыхзанимает надиске 1 Мбайт, может бытьсложно использоватьблоки, которыесодержали быболее одногоили двух элементов.Если каждыйиз блоков будетсодержать всегоодин или дваэлемента, тодля поиска иливставки элементапотребуетсяпроверитьмножествоблоков.
Прииспользованииоткрытой адресации(open addressing)хеш функцияиспользуетсядля непосредственноговычисленияположенияэлементовданных в массиве.Например, можноиспользоватьв качествехеш таблицымассив с нижниминдексом 0 иверхним 99. Тогдахеш функцияможет сопоставлятьключу со значениемK индексмассива, равныйK Mod 100. Приэтом элементсо значением1723 окажется втаблице на 23позиции. Затем, когда понадобитсянайти элемент1723, проверяется23 позиция в массиве.
    продолжение
--PAGE_BREAK--
==========295

Различныесхемы открытойадресациииспользуютразные методыдля формированиятестовыхпоследовательностей.В следующихразделахрассматриваютсятри наиболееважных метода: линейная, квадратичнаяи псевдослучайнаяпроверка.Линейнаяпроверка
Еслипозиция, накоторую отображаетсяновый элементв массиве, ужезанята, то можнопросто просмотретьмассив с этойточки до техпор, пока ненайдется незанятаяпозиция. Этотметод разрешенияконфликтовназываетсялинейной проверкой(linear probing), так как приэтом таблицапросматриваетсяпоследовательно.
Рассмотримснова пример, в котором имеетсямассив с нижнейграницей 0 иверхней границей99, и хеш функцияотображаетэлемент Kв позицию KMod 100. Чтобывставить элемент1723, вначале проверяетсяпозиция 23. Еслиэта ячейказаполнена, топроверяетсяпозиция 24. Еслиона также занята, то проверяютсяпозиции 25, 26, 27 итак далее дотех пор, покане найдетсясвободнаяячейка.
Чтобывставить новыйэлемент вхеш таблицу, применяетсявыбраннаятестоваяпоследовательностьдо тех пор, покане будет найденапустая ячейка.Чтобы найтиэлемент в таблице, применяетсявыбраннаятестоваяпоследовательностьдо тех пор, покане будет найденэлемент илипустая ячейка.Если пустаяячейка встретитсяраньше, значитэлемент в хеш таблицеотсутствует.
Можнозаписатькомбинированнуюфункцию проверкии хеширования:

Hash(K,P) = (K + P) Mod 100 гдеP = 0, 1, 2, ...

ЗдесьP —число элементовв тестовойпоследовательностидля K.Другими словами, для хешированияэлемента Kпроверяютсяэлементы Hash(K,0), Hash(K,1), Hash(K,2), … до техпор, пока ненайдется пустаяячейка.
Можнообобщить этуидею для созданиятаблицы размераNна основе массивас индексамиот 0 до N- 1. Хеш функциябудет иметьвид:

Hash(K,P) = (K + P) Mod N гдеP = 0, 1, 2, ...

Следующийкод показывает, как выполняетсяпоиск элементапри помощилинейной проверки:

PublicFunction LocateItem(Value As Long, pos AsInteger, _
probesAs Integer) As Integer
Dimnew_value As Long

probes= 1
pos= (Value Mod m_NumEntries)
Do
new_value= m_HashTable(pos)
'Элемент найден.
Ifnew_value = Value Then
LocateItem= HASH_FOUND
ExitFunction
EndIf
'Элемента втаблице нет.
Ifnew_value = UNUSED Or probes >= NumEntries Then
LocateItem= HASH_NOT_FOUND
pos= -1
ExitFunction
EndIf

pos= (pos + 1) Mod NumEntries
probes= probes + 1
Loop
EndFunction

ПрограммаLinearдемонстрируетоткрытую адресациюс линейнойпроверкой.Заполнив полеTable Size(Размер таблицы)и нажав на кнопкуCreate table(Создать таблицу), можно создаватьхеш таблицыразличныхразмеров. Затемможно ввестизначение элементаи нажать накнопку Add(Добавить) илиFind (Найти), чтобы вставитьили найти элементв таблице.
Чтобыдобавить втаблицу сразунесколькослучайныхзначений, введитечисло элементов, которые выхотите добавитьи максимальноезначение, котороеони могут иметьв области RandomItems (Случайныеэлементы), изатем нажмитена кнопку CreateItems (Создатьэлементы).
Послезавершенияпрограммойкакой либооперации онавыводит статусоперации (успешноеили безуспешноезавершение)и длину тестовойпоследовательности.Она также выводитсреднюю длинууспешной ибезуспешнойтестовойпоследовательностей.Программавычисляетсреднюю длинутестовойпоследовательности, выполняя поисквсех значенийот 1 до максимальногозначения втаблице.
В табл.11.1 приведенасредняя длинауспешных ибезуспешныхтестовыхпоследовательностей, полученныхв программеLinearдля таблицысо 100 ячейками, элементы вкоторых находятсяв диапазонеот 1 до 999. Из таблицывидно, чтопроизводительностьалгоритмападает по мерезаполнениятаблицы. Являетсяли производительностьприемлемой, зависит оттого, как используетсятаблица. Еслипрограмматратит большуючасть временина поиск значений, которые естьв таблице, топроизводительностьможет бытьнеплохой, дажеесли таблицапрактическизаполнена. Еслиже программачасто ищетзначения, которыхнет в таблице, то производительностьможет бытьочень низкой, если таблицапереполнена.
Какправило, хешированиеобеспечиваетприемлемуюпроизводительность, не расходуяпри этом слишкоммного памяти, если заполненоот 50 до 75 процентовтаблицы. Еслитаблица заполненабольше, чем на75 процентов, то производительностьпадает. Еслитаблица заполненаменьше, чем на50 процентов, то она занимаетбольше памяти, чем это необходимо.Это делаетоткрытую адресациюхорошим примеромпространственно временногокомпромисса.Увеличиваяхеш таблицу, можно уменьшитьвремя, необходимоедля вставкиили поискаэлементов.

=======297

@Таблица11.1. Длина успешнойи безуспешнойтестовыхпоследовательностей
Первичнаякластеризация
Линейнаяпроверка имеетодно неприятноесвойство, котороеназываетсяпервичнойкластеризацией(primary clustering).После добавлениябольшого числаэлементов втаблицу, возникаетконфликт междуновыми элементамии уже имеющимисякластерами, при этом длявставки новогоэлемента нужнообойти кластер, чтобы найтипустую ячейку.
Чтобыувидеть, какобразуютсякластеры, предположим, что вначалеимеется пустаяхеш таблица, которая можетсодержать Nэлементов. Есливыбрать случайноечисло и вставитьего в таблицу, то вероятностьтого, что элементзаймет любуюзаданную позициюP в таблице, равна 1/N.
Привставке второгослучайно выбранногоэлемента, онможет отобразитьсяна ту же позициюс вероятностью1/N. Из законфликта вэтом случаеон помещаетсяв позицию P+ 1. Также существуетвероятность1/N, что элементи должен располагатьсяв позиции P+ 1, и вероятность1/N, что ондолжен находитьсяв позиции P- 1. Во всех этихтрех случаяхновый элементрасполагаетсярядом с предыдущим.Таким образом, в целом существуетвероятность3/N того, что2 элемента окажутсярасположеннымивблизи другот друга, образуянебольшойкластер.
По мерероста кластеравероятностьтого, что следующиеэлементы будутрасполагатьсявблизи кластера, возрастает.Если в кластеренаходится дваэлемента, товероятностьтого, что очереднойэлемент присоединитсяк кластеру, равна 4/N, если в кластеречетыре элемента, то эта вероятностьравна 6/N, итак далее.
Чтоеще хуже, есликластер начинаетрасти, то егорост продолжаетсядо тех пор, покаон не столкнетсяс соседнимкластером. Двакластера сливаются, образуя кластереще большегоразмера, которыйрастет ещебыстрее, сливаетсяс другими кластерамии образует ещебольшие кластеры.

======298

В идеальномслучае хеш таблицадолжна бытьнаполовинупуста, и элементыв ней должнычередоватьсяс пустыми ячейками.Тогда с вероятностью50 процентовалгоритм сразуже найдет пустуюячейку длянового добавляемогоэлемента. Такжесуществует50 процентнаявероятностьтого, что оннайдет пустуюячейку послепроверки всеголишь двух позицийв таблице. Средняядлина тестовойпоследовательностиравна 0,5 * 1 + 0,5 * 2 = 1,5.
В наихудшемслучае всеэлементы втаблице будутсгруппированыв один гигантскийкластер. Приэтом все ещеесть 50 процентнаявероятностьтого, что алгоритмсразу найдетпустую ячейку, в которую можнопоместить новыйэлемент. Темне менее, еслиалгоритм ненайдет пустуюячейку на первомшаге, то поисксвободнойячейки потребуетгораздо большевремени. Еслиэлемент долженнаходитьсяна первой позициикластера, тоалгоритмупридется проверитьвсе элементыв кластере, чтобы найтисвободнуюячейку. В среднемдля вставкиэлемента притаком распределениипотребуетсягораздо большевремени, чемкогда элементыравномернораспределеныпо таблице.
На практике, степень кластеризациибудет находитьсямежду этимидвумя крайнимислучаями. Выможете использоватьпрограммуLinearдля исследованияэффекта кластеризации.Запуститепрограмму исоздайте хеш таблицусо 100 ячейками, а затем добавьте50 случайныхэлементов созначениямидо 999. Вы обнаружите, что образовалосьнесколькокластеров. Водном из тестов38 из 50 элементовстали частьюкластеров. Еслидобавить еще25 элементов ктаблице, тобольшинствоэлементов будутвходить в кластеры.В другом тесте70 из 75 элементовбыли сгруппированыв кластеры.Упорядоченнаялинейная проверка
Привыполнениипоиска в упорядоченномсписке методомполного перебора, можно остановитьпоиск, еслинайдется элементсо значениембольшим, чемискомое. Таккак при этомвозможноеположениеискомого элементауже позади, значит искомыйэлемент отсутствуетв списке.
Можноиспользоватьпохожую идеюпри поиске вхеш таблице.Предположим, что можноорганизоватьэлементы вхеш таблицетаким образом, что значенияв каждой тестовойпоследовательностинаходятся впорядке возрастания.Тогда при выполнениитестовойпоследовательностиво время поискаэлемента можнопрекратитьпоиск, есливстретитсяэлемент созначением, большим искомого.В этом случаепозиция, в которойдолжен был бынаходитьсяискомый элемент, уже осталасьпозади, и значитэлемента нетв таблице.

PublicFunction LocateItem(Value As Long, pos As Integer, _
probesAs Integer) As Integer
Dimnew_value As Long

probes= 1
pos= (Value Mod m_NumEntries)
Do
new_value= m_HashTable(pos)
'Элемента втаблице нет.
Ifnew_value = UNUSED Or probes > NumEntries Then
LocateItem= HASH_NOT_FOUND
pos= -1
ExitFunction
EndIf
'Элемент найденили его нет втаблице.
Ifnew_value >= Value Then Exit Do
pos= (pos + 1) Mod NumEntries
probes= probes + 1
Loop

IfValue = new_value Then
LocateItem= HASH_FOUND
Else
LocateItem= HASH_NOT_FOUND
EndIf
EndFunction

Длятого, чтобыэтот методработал, необходимоорганизоватьэлементы вхеш таблицетак, чтобы привыполнениитестовойпоследовательностиони встречалисьв возрастающемпорядке. Существуетдостаточнопростой методвставки элементов, который гарантируеттакое расположениеэлементов.
Когдав таблицу вставляетсяновый элемент, для него выполняетсятестоваяпоследовательность.Если найдетсясвободнаяячейка, то элементвставляетсяв эту позициюи процедуразавершена. Есливстречаетсяэлемент, значениекоторого большезначения новогоэлемента, тоони меняютсяместами ипродолжаетсявыполнениетестовойпоследовательностидля большегоэлемента. Приэтом можетвстретитьсяэлемент с ещебольшим значением.Тогда элементыснова меняютсяместами, ивыполняетсяпоиск новогоместоположениядля этого элемента.Этот процесспродолжаетсядо тех пор, пока, в конце концов, не найдетсясвободнаяячейка, приэтом возможнонесколькоэлементовменяются местами.

========299-300

PublicFunction InsertItem(ByVal Value As Long, pos As Integer,_ probesAs Integer) As Integer
Dimnew_value As Long
Dimstatus As Integer

'Проверить, заполнена литаблица.
Ifm_NumUnused
'Поиск элемента.
status= LocateItem(Value, pos, probes)
Ifstatus = HASH_FOUND Then
InsertItem= HASH_FOUND
Else
InsertItem= HASH_TABLE_FULL
pos= -1
EndIf
ExitFunction
EndIf

probes= 1
pos= (Value Mod m_NumEntries)
Do
new_value= m_HashTable(pos)

'Если значениенайдено, поискзавершен.
Ifnew_value = Value Then
InsertItem= HASH_FOUND
ExitFunction
EndIf

'Если ячейкасвободна, элементдолжен находитьсяв ней.
Ifnew_value = UNUSED Then
m_HashTable(pos)= Value
HashForm.TableControl(pos).Caption= Format$(Value)
InsertItem= HASH_INSERTED
m_NumUnused= m_NumUnused — 1
ExitFunction
EndIf
'Если значениев ячейке таблицыбольше значения
'элемента, поменятьих местами ипродолжить.
Ifnew_value > Value Then
m_HashTable(pos)= Value
Value= new_value
EndIf
pos= (pos + 1) Mod NumEntries
probes= probes + 1
Loop
EndFunction

ПрограммаOrderedдемонстрируетоткрытую адресациюс упорядоченнойлинейной проверкой.Она идентичнапрограммеLinear, но используетупорядоченнуюхеш таблицу.
В табл.11.2 приведенасредняя длинауспешной ибезуспешнойтестовыхпоследовательностейпри использованиилинейной иупорядоченнойлинейной проверок.Средняя длинауспешной проверкидля обоих методовпочти одинакова, но в случаенеуспехаупорядоченнаялинейная проверкавыполняетсянамного быстрее.Разница в особенностизаметна, еслихеш таблицазаполненаболее, чем на70 процентов.

=========301

@Таблица11.2. Длина поискапри использованиилинейной иупорядоченнойлинейной проверки

В обоихметодах длявставки новогоэлемента требуетсяпримерно одинаковоечисло шагов.Чтобы вставитьэлемент Kв таблицу, каждыйиз методовначинает спозиции (KModNumEntries)и перемещаетсяпо таблице дотех пор, покане найдет свободнуюячейку. Во времяупорядоченногохешированияможет потребоватьсяпоменять вставляемыйэлемент надругие в еготестовойпоследовательности.Если элементыпредставляютсобой записибольшого размера, то на это можетпотребоватьсябольше времени, особенно еслизаписи находятсяна диске иликаком либодругом медленномзапоминающемустройстве.
Упорядоченнаялинейная проверкаопределенноявляется лучшимвыбором, есливы знаете, чтопрограммепридется совершатьбольшое числобезуспешныхопераций поиска.Если программабудет частовыполнять поискэлементов, которых нетв таблице, илиэлементы таблицыимеют большойразмер и перемещатьих достаточносложно, то можнополучить лучшуюпроизводительностьпри использованиинеупорядоченнойлинейной проверки.Квадратичнаяпроверка
Одиниз способовуменьшитьпервичнуюкластеризациюсостоит в том, чтобы использоватьхеш функциюследующеговида:

Hash(K,P) = (K + P2) Mod N гдеP = 0, 1, 2, ...

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

=======302

На рис.11.8 показанахеш таблица, содержащаябольшой кластерэлементов. Нанем также показанытестовыепоследовательности, которые возникаютпри попыткевставить дваразличныхэлемента впозиции, занимаемыекластером. Обеэти тестовыепоследовательностизаканчиваютсяв точке, котораяне прилегаетк кластеру, поэтому послевставки этихэлементовразмер кластеране увеличивается.
Следующийкод демонстрируетпоиск элементас использованиемквадратичнойпроверки (quadraticprobing):

PublicFunction LocateItem(Value As Long, pos As Integer, probes As Integer)As Integer
Dimnew_value As Long

probes= 1
pos= (Value Mod m_NumEntries)
Do
new_value= m_HashTable(pos)
'Элемент найден.
Ifnew_value = Value Then
LocateItem= HASH_FOUND
ExitFunction
EndIf
'Элемента нетв таблице.
Ifnew_value = UNUSED Or probes > NumEntries Then
LocateItem= HASH_NOT_FOUND
pos= -1
ExitFunction
EndIf

pos= (Value + probes * probes) Mod NumEntries
probes= probes + 1
Loop
EndFunction

ПрограммаQuadдемонстрируетоткрытую адресациюс использованиемквадратичнойпроверки. ОнааналогичнапрограммеLinear, но используетквадратичную, а не линейнуюпроверку.
В табл.11.3 приведенасредняя длинатестовыхпоследовательностей, полученныхв программахLinearи Quadдля хеш таблицысо 100 ячейками, значения элементовв которой находятсяв диапазонеот 1 до 999. Квадратичнаяпроверка обычнодает лучшиерезультаты.

@Рис.11.8. Квадратичнаяпроверка

======303

@Таблица11.3. Длина поискапри использованиилинейной иквадратичнойпроверки

Квадратичнаяпроверка такжеимеет некоторыенедостатки.Из за способаформированиятестовойпоследовательности, нельзя гарантировать, что она обойдетвсе ячейки втаблице, чтоозначает, чтоиногда в таблицунельзя будетвставить элемент, даже если онане заполненадо конца.
Например, рассмотримнебольшуюхеш таблицу, состоящую всегоиз шести ячеек.Тестоваяпоследовательностьдля числа 3 будетследующей:

3
3+ 12= 4 = 4 (Mod 6)
3+ 22= 7 = 1 (Mod 6)
3+ 32= 12 = 0 (Mod 6)
3+ 42= 19 = 1 (Mod 6)
3+ 52= 28 = 4 (Mod 6)
3+ 62= 39 = 3 (Mod 6)
3+ 72= 52 = 4 (Mod 6)
3+ 82= 67 = 1 (Mod 6)
3+ 92= 84 = 0 (Mod 6)
3+ 102= 103 = 1 (Mod6)
итак далее.

Этатестоваяпоследовательностьобращаетсяк позициям 1 и4 дважды передтем, как обратитьсяк позиции 3, иникогда непопадает впозиции 2 и 5. Чтобыпронаблюдатьэтот эффект, создайте впрограмме Quadхеш таблицус шестью ячейками, а затем вставьтеэлементы 1, 3, 4, 6 и9. Программаопределит, чтотаблица заполненацеликом, хотядве ячейки иосталисьнеиспользованными.Тестоваяпоследовательностьдля элемента9 не обращаетсяк элементам2 и 5, поэтомупрограмма неможет вставитьв таблицу новыйэлемент.
    продолжение
--PAGE_BREAK--
=======304

Можнопоказать, чтоквадратичнаятестоваяпоследовательностьбудет обращаться, по меньшеймере, к N/2ячеек таблицы, если размертаблицы N —простое число.Хотя при этомгарантируетсянекоторыйуровень производительности, все равно могутвозникнутьпроблемы, еслитаблица почтизаполнена. Таккак производительностьдля почти заполненнойтаблицы в любомслучае сильнопадает, то возможнолучше будетпросто увеличитьразмер хеш-таблицы, а не беспокоитьсяо том, сможетли тестоваяпоследовательностьнайти свободнуюячейку.
Не стольочевиднаяпроблема, котораявозникает припримененииквадратичнойпроверки, заключаетсяв том, что хотяона устраняетпервичнуюкластеризацию, во время нееможет возникатьпохожая проблема, которая называетсявторичнойкластеризацией(secondary clustering).Если два элементаотображаютсяв одну ячейку, для них будетвыполнятьсяодна и так жетестоваяпоследовательность.Если множествоэлементовотображаютсяна одну из ячеектаблицы, ониобразуют вторичныйкластер, которыйраспределенпо хеш таблице.Если появляетсяновый элементс тем же самымначальнымзначением, длянего приходитсявыполнятьдлительнуютестовуюпоследовательность, прежде чем онобойдет элементыво вторичномкластере.
На рис.11.9 показанахеш таблица, которая можетсодержать 10ячеек. В таблиценаходятсяэлементы 2, 12, 22 и32, которые всеизначальноотображаютсяв позицию 2. Еслипопытатьсявставить втаблицу элемент42, то нужно будетвыполнитьдлительнуютестовуюпоследовательность, которая обойдетвсе эти элементы, прежде чемнайдет свободнуюячейку.Псевдослучайнаяпроверка
Степенькластеризациирастет, еслив кластер добавляютсяэлементы, которыеотображаютсяна уже занятыекластеромячейки. Вторичнаякластеризациявозникает, когда для элементов, которые первоначальнодолжны заниматьодну и ту жеячейку, выполняетсяодна и та жетестоваяпоследовательность, и образуетсявторичныйкластер, распределенныйпо хеш таблице.Можно устранитьоба эти эффекта, если сделатьтак, чтобы дляразных элементоввыполнялисьразличныетестовыепоследовательности, даже если элементыпервоначальнои должны былизанимать однуи ту же ячейку.
Одиниз способовсделать этозаключаетсяв использованиив тестовойпоследовательностигенераторапсевдослучайныхчисел. Для вычислениятестовойпоследовательностидля элемента, его значениеиспользуетсядля инициализациигенератораслучайныхчисел. Затемдля построениятестовойпоследовательностииспользуютсяпоследовательныеслучайныечисла, получаемыена выходе генератора.Это называетсяпсевдослучайнойпроверкой(pseudo randomprobing).
Когдапозднее требуетсянайти элементв хеш таблице, генераторслучайных чиселснова инициализируетсязначениемэлемента, приэтом на выходегенераторамы получим туже самую последовательностьчисел, котораяиспользоваласьдля вставкиэлемента втаблицу. Используяэти числа, можновоссоздатьисходную тестовуюпоследовательностьи найти элемент.

@Рис.11.9. Вторичнаякластеризация

==========305

Еслииспользуетсякачественныйгенераторслучайныхчисел, то разныезначения элементовбудут даватьразличныеслучайные числаи соответственноразные тестовыепоследовательности.Даже если двазначения изначальноотображаютсяна одну и ту жеячейку, то следующиепозиции в тестовойпоследовательностибудут уже различными.В этом случаев хеш таблицене будет возникатьпервичная иливторичнаякластеризация.
Можнопроинициализироватьгенераторслучайных чиселVisual Basic, используяначальноечисло, при помощидвух строчеккода:

Rnd-1
Randomizeseed_value

ОператорRndдает одну и туже последовательностьчисел послеинициализацииодним и тем женачальнымчислом. Следующийкода показывает, как можно выполнятьпоиск элементас использованиемпсевдослучайнойпроверки:

PublicFunction LocateItem(Value As Long, pos As Integer,_
probesAs Integer) As Integer
Dimnew_value As Long

'Проинициализироватьгенераторслучайныхчисел.
Rnd-1
RandomizeValue

probes= 1
pos= Int(Rnd * m_NumEntries)
Do
new_value= m_HashTable(pos)

'Элемент найден.
Ifnew_value = Value Then
LocateItem= HASH_FOUND
ExitFunction
EndIf

'Элемента нетв таблице.
Ifnew_value = UNUSED Or probes > NumEntries Then
LocateItem= HASH_NOT_FOUND
pos= -1
ExitFunction
EndIf

pos= Int(Rnd * m_NumEntries)
probes= probes + 1
Loop
EndFunction

=======306

ПрограммаRandдемонстрируетоткрытую адресациюс псевдослучайнойпроверкой. ОнааналогичнапрограммамLinearи Quad, но используетпсевдослучайную, а не линейнуюили квадратичнуюпроверку.
В табл.11.4 приведенапримернаясредняя длинатестовойпоследовательности, полученнойв программахQuadили Randдля хеш таблицысо 100 ячейкамии элементами, значения которыхнаходятся вдиапазоне от1 до 999. Обычнопсевдослучайнаяпроверка даетнаилучшиерезультаты, хотя разницамежду псевдослучайнойи квадратичнойпроверкамине так велика, как между линейнойи квадратичной.
Псевдослучайнаяпроверка такжеимеет своинедостатки.Так как тестоваяпоследовательностьвыбираетсяпсевдослучайно, нельзя точнопредсказать, насколькобыстро алгоритмобойдет всеэлементы втаблице. Еслитаблица меньше, чем число возможныхпсевдослучайныхзначений, тосуществуетвероятностьтого, что тестоваяпоследовательностьобратится кодному значениюнесколько раздо того, какона выберетдругие значенияв таблице. Возможнотакже, что тестоваяпоследовательностьбудет пропускатькакую либоячейку в таблицеи не сможетвставить новыйэлемент, дажеесли таблицане заполненадо конца.
Также, как и в случаеквадратичнойпроверки, этиэффекты могутвызвать затруднения, только еслитаблица почтизаполнена. Вэтом случаеувеличениетаблицы даетгораздо большийприрост производительности, чем поискнеиспользуемыхячеек таблицы.

@Рис.11.4. Длина поискапри использованииквадратичнойи псевдослучайнойпроверки

=======307
Удалениеэлементов
Удалениеэлементов изхеш таблицы, в которойиспользуетсяоткрытая адресация, выполняетсяне так просто, как удалениеих из таблицы, использующейсвязные спискиили блоки. Простоудалить элементиз таблицынельзя, так какон может находитьсяв тестовойпоследовательностидругого элемента.
Предположим, что элементA находитсяв тестовойпоследовательностиэлемента B.Если удалитьиз таблицыэлемент A, найти элементB будетневозможно.Во время поискаэлемента Bвстретитсяпустая ячейка, которая осталасьпосле удаленияэлемента A, поэтому будетсделан неправильныйвывод о том, что элементB отсутствуетв таблице.
Вместоудаления элементаиз хеш таблицыможно простопометить егокак удаленный.Можно использоватьэту ячейкупозднее, еслиона встретитсяво время выполнениявставки новогоэлемента втаблицу. Еслипомеченныйэлемент встречаетсяво время поискадругого элемента, он простоигнорируетсяи тестоваяпоследовательностьпродолжится.
Послетого, как большоечисло элементовбудет помеченокак удаленные, в хеш таблицеможет оказатьсямножествонеиспользуемыхячеек, и припоиске элементовдостаточномного временибудет уходитьна пропускудаленныхэлементов. Вконце концов, может потребоватьсярехешированиетаблицы дляосвобождениянеиспользуемойпамяти.Рехеширование
Чтобыосвободитьудаленныеэлементы изхеш таблицы, можно выполнитьее рехеширование(rehashing) на месте.Чтобы этоталгоритм могработать, нужноиметь какой тоспособ дляопределения, было ли выполненорехешированиеэлемента. Простейшийспособ сделатьэто — определитьэлементы в видеструктур данных, содержащихполе Rehashed.

TypeItemType
ValueAs Long
RehashedAs Boolean
EndType

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

======308
Изменениеразмера хеш таблиц
Еслихеш таблицастановитсяпочти заполненной, производительностьзначительнопадает. В этомслучае можетпонадобитьсяувеличениеразмера таблицы, чтобы в нейбыло большеместа для элементов.И наоборот, если в таблицеслишком малоячеек, можетпотребоватьсяуменьшить ее, чтобы освободитьзанимаемуюпамять. Используяметоды, похожиена те, которыеиспользовалисьпри рехешированиитаблицы наместе, можноувеличиватьи уменьшатьразмер хеш таблицы.
Чтобыувеличитьхеш таблицу, вначале размермассива, в которомона находится, увеличиваетсяпри помощиоператора DimPreserve.Затем выполняетсярехешированиетаблицы, приэтом элементымогут заниматьячейки в созданнойсвободнойобласти в концетаблицы. Послезавершениярехешированиятаблица будетготова к использованию.
Чтобыуменьшитьразмер таблицы, вначале определим, сколько элементовдолжно содержатьсяв массиве таблицыпосле уменьшения.Затем выполняемрехешированиетаблицы, причемэлементы помещаютсятолько в уменьшеннуючасть таблицы.После завершениярехешированиявсех элементов, размер массивауменьшаетсяпри помощиоператора ReDimPreserve.
Следующийкод демонстрируетрехешированиетаблицы сиспользованиемлинейной проверки.Код для рехешированиятаблицы сиспользованиемквадратичнойили псевдослучайнойпроверки выглядитпочти так же:

PublicSub Rehash()
Dimi As Integer
Dimpos As Integer
Dimprobes As Integer
DimValue As Long
Dimnew_value As Long

'Пометить всеэлементы какнерехешированные.
Fori = 0 To NumEntries — 1
m_HashTable(i).Rehashed= False
Nexti
'Поиск нерехешированныхэлементов.
Fori = 0 To NumEntries — 1
IfNot m_HashTable(i).Rehashed Then
Value= m_HashTable(i).Value
m_HashTable(i).Value= UNUSED

IfValue DELETED And Value UNUSED Then
'Выполнитьтестовуюпоследовательность
'для этого элемента, пока не найдетсясвободная,
'удаленная илинерехешированнаяячейка.
probes= 0
Do
pos= (Value + probes) Mod NumEntries
new_value= m_HashTable(pos).Value
'Если ячейкасвободна илипомечена как
'удаленная, поместитьэлемент в нее.
Ifnew_value = UNUSED Or _
new_value= DELETED _
Then
m_HashTable(pos).Value= Value
m_HashTable(pos).Rehashed= True
ExitDo
EndIf
'Если ячейкане помеченакак рехешированная,
'поменять ихместами и продолжить.
IfNot m_HashTable(pos).Rehashed Then
m_HashTable(pos).Value= Value
m_HashTable(pos).Rehashed= True
Value= new_value
probes= 0
Else
probes= probes + 1
EndIf
Loop
EndIf
EndIf
Nexti
EndSub

ПрограммаRehashиспользуетоткрытую адресациюс линейнойпроверкой. ОнааналогичнапрограммеLinear, но позволяеттакже помечатьобъекты какудаленные ивыполнятьрехешированиетаблицы.Резюме
Различныетипы хеш таблиц, описанные вэтой главе, имеют своипреимуществаи недостатки.
Дляхеш таблиц, которые используютсвязные спискиили блоки можнолегко изменятьразмер таблицыи удалять изнее элементы.Использованиеблоков такжепозволяет легкоработать стаблицами надиске, позволяясчитать за однообращение кдиску сразумножествоэлементовданных. Тем неменее, оба этиметода являютсяболее медленными, чем открытаяадресация.
Линейнаяпроверка простаи позволяетдостаточнобыстро вставлятьи удалять элементыиз таблицы.Применениеупорядоченнойлинейной проверкипозволяетбыстрее, чемв случае неупорядоченнойлинейной проверки, установить, что элементотсутствуетв таблице. Сдругой стороны, вставку элементовв таблицу приэтом выполнитьсложнее.
Квадратичнаяпроверка позволяетизбежатькластеризации, которая характернадля линейнойпроверки, ипоэтому обеспечиваетболее высокуюпроизводительность.Псевдослучайнаяпроверка обеспечиваетеще более высокуюпроизводительность, так как приэтом удаетсяизбавитьсякак от первичной, так и от вторичнойкластеризации.
В табл.11.5 приведеныпреимуществаи недостаткиразличныхметодов хеширования.

======310

@Таблица11.5. Преимуществаи недостаткиразличныхметодов хеширования

Выборнаилучшегометода хешированиядля данногоприложениязависит отданных задачии способов ихиспользования.При примененииразных схемдостигаютсяразличныекомпромиссымежду занимаемойпамятью, скоростьюи простотойизменений.Табл. 11.5 можетпомочь вамвыбрать наилучшийалгоритм длявашего приложения.

=======311
Глава12. Сетевые алгоритмы
В 6 и 7главах обсуждалисьалгоритмыработы с деревьями.Данная главапосвящена болееобщей темесетей. Сетииграют важнуюроль во многихприложениях.Их можно использоватьдля моделированиятаких объектов, как сеть улиц, телефоннаяили электрическаясеть, водопровод, канализация, водосток, сетьавиаперевозокили железныхдорог. Менееочевидна возможностьиспользованиясетей для решениятаких задач, как разбиениена районы, составлениерасписанияметодом критическогопути, планированиеколлективнойработы илираспределенияработы.Определения
Как ив определениидеревьев, сетью(network) или графом(graph) называетсянабор узлов(nodes), соединенныхребрами (edges)или связями(links). Для графа, в отличие отдерева, не определенопонятие родительскогоили дочернегоузла.
С ребрамисети может бытьсвязано соответствующеенаправление, тогда в этомслучае сетьназываетсяориентированнойсетью (directednetwork). Для каждойсвязи можнотакже определитьее цену (cost).Для сети дорог, например, ценаможет бытьравна времени, которое займетпроезд по отрезкудороги, представленномуребром сети.В телефоннойсети цена можетбыть равнакоэффициентуэлектрическихпотерь в кабеле, представленномсвязью. На рис.12.1 показананебольшаяориентированнаясеть, в которойчисла рядомс ребрамисоответствуютцене ребра.
Путем(path) междуузлами Aи B называетсяпоследовательностьребер, котораясвязывает дваэтих узла междусобой. Еслимежду любымидвумя узламисети есть небольше одногоребра, то путьможно однозначноописать, перечисливвходящие в негоузлы. Так кактакое описаниепроще представитьнаглядно, топути по возможностиописываютсятаким образом.На рис. 12.1 путь, проходящийчерез узлы B,E, F, G,Eи D, соединяетузлы B и D.
Циклом(cycle) называетсяпуть которыйсвязывает узелс ним самим.Путь E, F,G, E нарис. 12.1 являетсяциклом. Путьназываетсяпростым (simple), если он не содержитциклов. ПутьB, E, F,G, E, Dне являетсяпростым, таккак он содержитцикл E, F,G, E.
Еслисуществуеткакой либопуть междудвумя узлами, то долженсуществоватьи простой путьмежду ними.Этот путь можнонайти, еслиудалить всециклы из исходногопути. Например, если заменитьцикл E, F,G, E в путиB, E, F,G, E, Dна узел E, то получитсяпростой путьB, E, D, связывающийузлы B и D.

=======313

@Рис.12.1. Ориентированнаясеть с ценойребер

Сетьназываетсясвязной(connected), еслимежду любымидвумя узламисуществуетхотя бы одинпуть. В ориентированнойсети не всегдаочевидно, являетсяли сеть связной.На рис. 12.2 сетьслева являетсясвязной. Сетьсправа не являетсясвязной, таккак не существуетпути из узлаE в узел C.Представлениясети
В 6 главебыло описанонесколькопредставленийдеревьев. Большинствоиз них применимотакже и дляработы с сетями.Например, представленияполными узлами, списком потомков(списком соседейдля сетей) илинумерациейсвязей такжемогут использоватьсядля хранениясетей. За описаниемэтих представленийобратитеськ 6 главе.

@Рис.12.2. Связная (слева)и несвязная(справа) сети

======314

Дляразличныхприложениймогут лучшеподходитьразные представлениясети. Представлениеполными узламиобеспечиваетхорошие результаты, если каждыйузел в сетисвязан с небольшимчислом ребер.Представлениесписком соседнихузлов обеспечиваетбольшую гибкость, чем представлениеполными узлами, а представлениенумерациейсвязей, хотяего сложнеемодифицировать, обеспечиваетболее высокуюпроизводительность.
Кромеэтого, некоторыевариантыпредставленияребер могутупроститьработу с определеннымитипами сетей.Эти представленияиспользуютодин класс дляузлов и другой —для представлениясвязей. Применениекласса длясвязей облегчаетработу со свойствамисвязей, такими, как цена связи.
Например, ориентированнаясеть с ценойсвязей можетиспользоватьследующееопределениядля классаузла:
    продолжение
--PAGE_BREAK--
PublicId As Integer ' Номерузла.
PublicLinks As Collection ' Связи, ведущиек соседнимузлам.

Можноиспользоватьследующееопределениекласса связей:

PublicToNode As NetworkNode ' Узелна другомконце связи.
PublicCost As Integer ' Ценасвязи.

Используяэти определения, программа можетнайти связьс наименьшейценой, используяследующий код:

Dimlink As NetworkLink
Dimbest_link As NetworkLink
Dimbest_cost As Integer

best_cost= 32767
ForEach link In node.Links
Iflink.cost
Setbest_link = link
best_cost= link.cost
EndIf
Nextlink

Классыnodeи linkчасто расширяютсядля удобстваработы с конкретнымиалгоритмами.Например, кклассу nodeчасто добавляетсяфлаг Marked.Если программаобращаетсяк узлу, то онаустанавливаетзначение поляMarkedравным true, чтобы знать, что узел ужебыл проверен.
Программа, управляющаянеориентированнойсетью, можетиспользоватьнемного другоепредставление.Класс nodeостается темже, что и раньше, но класс linkвключает ссылкуна оба узла наконцах связи.

PublicNode1 As NetwokNode ' Один изузлов на концесвязи.
PublicNode2 As NetwokNode ' Другойузел.
PublicCost As Integer ' Ценасвязи.

Длянеориентированнойсети, предыдущеепредставлениеиспользовалобы два объектадля представлениякаждой связи —по одному длякаждого изнаправленийсвязи. В новойверсии каждаясвязь представленаодним объектом.Это представлениедостаточнонаглядно, поэтомуоно используетсядалее в этойглаве.

=======315

Используяэто представление, программаNetEditпозволяетоперироватьнеориентированнымисетями с ценойсвязей. МенюFile (Файл)позволяетзагружать исохранять сетив файлах. Командыв меню Edit(Правка) позволяютвам вставлятьи удалять узлыи связи. На рис.12.3 показано окнопрограммыNetEdit.
ДиректорияOldSrc\Ch12содержит программы, которые используютпредставлениенумерациейсвязей. Этипрограммынемного сложнеепонять, но ониобычно работаютбыстрее. Онине описаны втексте, ноиспользованныев них методыпохожи на те, которые применялисьв программах, написанныхдля 4 версииVisual Basic.Например, обепрограммыSrc\Ch12\PathsиOldSrc\Ch12\Pathsнаходят кратчайшиймаршрут, используяописанный нижеалгоритм установкиметок. Основноеотличие междуними заключаетсяв том, что перваяпрограммаиспользуетколлекции иклассы, а вторая —псевдоуказателии представлениенумерациейсвязей.Оперированиеузлами и связями
Кореньдерева — этоединственныйузел, не имеющийродителя. Можнонайти любойузел в сети, начав от корняи следуя поуказателямна дочерниеузлы. Такимобразом, узелпредставляетоснованиедерева. Есливвести переменную, которая будетсодержатьуказатель накорневой узел, то впоследствииможно будетполучить доступко всем узламв дереве.
Сетине всегда содержатузел, которыйзанимает такоеособое положение.В несвязнойсети может несуществоватьспособа обойтивсе узлы посвязям, начавс одного узла.
Поэтомупрограммы, работающиес сетями, обычносодержат полныйсписок всехузлов в сети.Программа такжеможет хранитьполный списоквсех связей.При помощи этихсписков можнолегко выполнитькакие либодействия надвсеми узламиили связямив сети. Например, если программахранит указателина узлы и связив коллекцияхNodesи Links, она может вывестисеть на экранпри помощиследующегометода:

@Рис.12.3. ПрограммаNetEdit

=======316

Dimnode As NetworkNode
dimlink As NetworkLink
ForEach link in links
'Нарисоватьсвязь.
:
Nextlink

ForEach node in nodes
'Нарисоватьузел.
:
Nextnode

ПрограммаNetEditиспользуетколлекции Nodesи Linksдля выводасетей на экран.Обходысети
Обходсети выполняетсяаналогичнообходу дерева.Можно обходитьсеть, используялибо обход вглубину, либообход в ширину.Обход в ширинуобычно похожна прямой обходдерева, хотядля сетей можноопределитьтакже обратныйи даже симметричныйобход.
Алгоритмдля выполненияпрямого обходадвоичногодерева, описанныйв 6 главе, формулируетсятак:
Обратиться к узлу.
Выполнить рекурсивный прямой обход левого поддерева.
Выполнить рекурсивный прямой обход правого поддерева.
В деревемежду связаннымимежду собойузлами существуетотношениеродитель потомок.Так как алгоритмначинаетсяс корневогоузла и всегдавыполняетсясверху вниз, он не обращаетсядважды ни кодному узлу.
В сетиузлы не обязательносвязаны в направлениисверху вниз.Если попытатьсяприменить ксети алгоритмпрямого обхода, может возникнутьбесконечныйцикл.
Чтобыизбежать этого, алгоритм долженпомечать узелпосле обращенияк нему, при этомпри поиске всоседних узлах, обращениепроисходиттолько к узлам, которые ещене были помечены.После того, какалгоритм завершитработу, всеузлы в сетибудут помечены(если сеть являетсясвязной). Алгоритмпрямого обходасети формулируетсятак:
Пометить узел.
Обратиться к узлу.
Выполнить рекурсивный обход не помеченных соседних узлов.

========317

В VisualBasic можнодобавить флагMarkedк классу NetworkNode.

PublicId As Long
PublicMarked As Boolean
PublicLinks As Collection

КлассNetworkNodeможет включатьоткрытую процедурудля обходасети, начинаяс этого узла.Процедура узлаPreorderPrintобращаетсяко всем непомеченнымузлам, которыедоступны изданного узла.Если сеть являетсясвязной, то притаком обходепроизойдетобращение ковсем узламсети.

PublicSub PreorderPrint()
Dimlink As NoworkLink
Dimnode As NetworkNode

'Пометить узел.
Marked= True

'Обратитьсяк непомеченнымузлам.
ForEach link In Links
'Найти соседнийузел.
Iflink.Node1 Is Me Then
Setnode = link.Node2
Else
Setnode = link.Node1
EndIf

'Определить, требуется лиобращение ксоседнему узлу.
IfNot node.Marked Then node.PreorderPrint
Nextlink
EndSub

Таккак эта процедуране обращаетсяни к одномуузлу дважды, то коллекцияобходимыхсвязей не содержитциклов и образуетдерево.
Еслисеть являетсясвязной, тодерево будетобходить всеузлы сети. Таккак это деревоохватываетвсе узлы сети, то оно называетсяостовным деревом(spanning tree).На рис. 12.4 показананебольшая сетьс остовнымдеревом с корнемв узле A, которое изображеножирными линиями.
Можноиспользоватьпохожий подходс пометкойузлов дляпреобразованияобхода деревав ширину в сетевойалгоритм. Алгоритмобхода дереваначинаетсяс помещениякорневого узлав очередь. Затемпервый узелудаляется изочереди, происходитобращение кузлу, и затемв конце очередипомещаютсяего дочерниеузлы. Затемэтот процессповторяетсядо тех пор, покаочередь неопустеет.

======318

@Рис.12.4. Остовное дерево

В алгоритмеобхода сетинужно вначалеубедиться, чтоузел не проверялсяраньше или онуже не находитсяв очереди. Дляэтого мы помечаемкаждый узел, который помещаетсяв очередь. Сетеваяверсия этогоалгоритмавыглядит так:
Пометить первый узел (который будет корнем остовного дерева) и добавить его в конец очереди.
Повторять следующие шаги до тех пор, пока очередь не опустеет:
Удалить из очереди первый узел и обратиться к нему.
Для каждого из непомеченных соседних узлов, пометить его и добавить в конец очереди.
Следующаяпроцедурапечатает списокузлов сети впорядке обходав ширину:

PublicSub BreadthFirstPrint(root As NetworkNode)
Dimqueue As New Collection
Dimnode As NetworkNode
Dimneighbor As NetworkNode
Dimlink As NetworkLink

'Поместитькорень в очередь.
root.Marked= True
queue.Addroot

'Многократнопомещать верхнийэлемент в очередь
'пока очередьне опустеет.
DoWhile queue.Count > 0
'Выбрать следующийузел из очереди.
Setnode = queue.Item(1)
queue.Remove1

'Обратитьсяк узлу.
Printnode.Id

'Добавить вочередь всенепомеченныесоседние узлы.
ForEach link In node.Links
'Найти соседнийузел.
Iflink.Node1 Is Me Then
Setneighbor = link.Node2
Else
Setneighbor = link.Node1
EndIf

'Проверить, нужно ли обращениек соседнемуузлу.
IfNot neighbor.Marked Then queue.Add neighbor
Nextlink
Loop
EndSub
Наименьшиеостовные деревья
Еслизадана сетьс ценой связей, то наименьшимостовным деревом(minimal spanningtree) называетсяостовное дерево, в котором суммарнаяцена всех связейв дереве будетнаименьшей.Наименьшееостовное деревоможно использовать, чтобы связатьвсе узлы в сетипутем с наименьшейценой.
Например, предположим, что требуетсяразработатьтелефоннуюсеть, котораядолжна соединитьшесть городов.Можно проложитьмагистральныйкабель междувсеми парамигородов, но этобудет неоправданнодорого. Меньшуюстоимость будетиметь решение, при которомгорода будутсоединенысвязями, которыесодержатсяв наименьшемостовном дереве.На рис. 12.5 показанышесть городов, каждые два изкоторых соединенымагистральнымкабелем. Жирнымилиниями нарисованонаименьшееостовное дерево.
Заметьте, что сеть можетиметь нескольконаименьшихостовных деревьев.На рис. 12.6 показаныдва изображениясети с двумяразличныминаименьшимиостовнымидеревьями, которые нарисованыжирными линиями.Полная ценаобоих деревьевравна 32.

@Рис.12.5. Магистральныетелефонныекабели, связывающиешесть городов

========320

@Рис.12.6. Два различныхнаименьшихостовных деревадля одной сети

Существуетпростой алгоритмпоиска наименьшегоостовногодерева длясети. Вначалепоместим востовное дереволюбой узел.Затем найдемсвязь с наименьшейценой, котораясоединяет узелв дереве с узлом, который ещене помещен вдерево. Добавимэту связь исоответствующийузел в дерево.Затем эта процедураповторяетсядо тех пор, покавсе узлы неокажутся вдереве.
Этоталгоритм похожна эвристикувосхожденияна холм, описаннуюв 8 главе. На каждомшаге оба алгоритмаизменяют решение, пытаясь егомаксимальноулучшить. Алгоритмостовногодерева на каждомшаге выбираетсвязь с наименьшейценой, котораядобавляет новыйузел в дерево.В отличие отэвристикивосхожденияна холм, котораяне всегда находитнаилучшеерешение, этоталгоритмгарантированнонаходит наименьшееостовное дерево.
Подобныеалгоритмы, которые находятглобальныйоптимум, припомощи сериилокально оптимальныхприближенийназываютсяпоглощающимиалгоритмами(greedyalgorithms). Можнопредставлятьсебе поглощающиеалгоритмы какалгоритмы типавосхожденияна холм, неявляющиесяпри этом эвристиками —они гарантированнонаходят наилучшеевозможноерешение.
Алгоритмнаименьшегоостовногодерева используетколлекцию дляхранения спискасвязей, которыемогут бытьдобавлены костовномудереву. Вначалеалгоритм помещаетв этот списоксвязи корневогоузла. Затемпроводитсяпоиск связис наименьшейценой в этомсписке. Чтобымаксимальноускорить поиск, программа можетиспользоватьприоритетнуюочередь типаописанной в9 главе. Илинаоборот, чтобыупроститьреализацию, программа можетиспользоватьдля хранениясписка возможныхсвязей коллекцию.
Еслиузел на другомконце связиеще не находитсяв остовномдереве, то программадобавляет егои соответствующуюсвязь в дерево.Затем она добавляетсвязи, выходящиеиз нового узла, в список возможныхузлов.
Алгоритмиспользуетфлаг Usedв классе link, чтобы определить, попадала лиэта связь ранеев список возможныхсвязей. Еслида, то она незаносится вэтот списокснова.
Можетоказаться, чтосписок возможныхсвязей опустеетдо того, каквсе узлы будутдобавлены востовное дерево.В этом случаесеть являетсянесвязной, ине существуетпуть, которыйсвязываеткорневой узелсо всеми остальнымиузлами сети.

=========321

PrivateSub FindSpanningTree(root As SpanNode)
Dimcandidates As New Collection
Dimto_node As SpanNode
Dimlink As SpanLink
Dimi As Integer
Dimbest_i As Integer
Dimbest_cost As Integer
Dimbest_to_node As SpanNode

Ifroot Is Nothing Then Exit Sub
'Сброситьфлаг Marked длявсех узлов ифлаги
'Used и InSpanningTree длявсех связей.
ResetSpanningTree

'Начать с корняостовногодерева.
root.Marked= True
Setbest_to_node = root

Do
'Добавитьсвязи последнегоузла в список
'возможныхсвязей.
ForEach link In best_to_node.Links
IfNot link.Used Then
candidates.Addlink
link.Used= True
EndIf
Nextlink

'Найти самуюкороткую связьв списке возможных
'связей, котораяведет к узлу, которого ещенет
'в дереве.
best_i= 0
best_cost= INFINITY
i= 1
DoWhile i
Setlink = candidates(i)
Iflink.Node1.Marked Then
Setto_node = link.Node2
Else
Setto_node = link.Node1
EndIf
Ifto_node.Marked Then
'Связь соединяетдва узла, которые
'оба находятсяв дереве.
'Удалить ее изсписка возможныхсвязей.
candidates.Removei
Else
Iflink.Cost
best_i= i
best_cost= link.Cost
Setbest_to_node = to_node
EndIf
i= i + 1
EndIf
Loop
'Если большене осталосьсвязей, которыеможно
'было бы добавить, то мы сделаливсе, что могли.
Ifbest_i

'Добавить наилучшуюсвязь и узелна ее конце вдерево.
Setlink = candidates(best_i)
link.InSpanningTree= True
candidates.Removebest_i
best_to_node.Marked= True
Loop

GotSpanningTree= True
'Перерисоватьсеть.
DrawNetwork
EndSub

Этоталгоритм проверяеткаждую связьне более одногораза. При проверкекаждой связи, она добавляетсяв список возможныхсвязей, а затемудаляется изнего. Если этотсписок находитсяв приоритетнойочереди наоснове пирамид, то для вставкиили удаленияэлемента изочереди потребуетсявремя порядкаO(log(N)), где — числосвязей в сети.В этом случаеполное времявыполненияалгоритма будетпорядка O(N* log(N)).
Еслисписок возможныхсвязей находитсяв коллекции, как в вышеприведенномкоде, то дляпоиска в спискесвязи с наименьшейценой потребуетсявремя порядкаO(N), приэтом полноевремя выполненияалгоритма будетпорядка O(N2).Для малых Nпроизводительностьбудет приемлемой.Если же числосвязей в сетидостаточновелико, то списоквозможныхсвязей следуетхранить вприоритетнойочереди, а нев коллекции.
ПрограммаSpanиспользуетэтот алгоритмдля поисканаименьшегоостовногодерева. ЭтапрограммааналогичнапрограммеNetEdit.Она позволяетзагружать, редактироватьи сохранятьна диске файлы, представляющиесеть. Если выбратькакой либоузел в программедвойным щелчкоммыши, то программанайдет и выведетна экран наименьшееостовное деревос корнем в этомузле. На рис.12.7 показано окнопрограммы Span, в котором показанонаименьшееостовное деревос корнем в узле9.

======322-323

@Рис.12.7. ПрограммаSpan
Кратчайшиймаршрут
Алгоритмыпоиска кратчайшегомаршрута, которыеобсуждаютсяв следующихразделах, находятвсе кратчайшиепути из заданнойточки до всехостальных точексети, при этомпредполагается, что сеть являетсясвязанной.Набор связей, используемыйвсеми кратчайшимимаршрутами, называетсядеревом кратчайшегомаршрута(shortest pathtree).
На рис.12.8 показано дерево, в котором деревократчайшегомаршрута скорнем в узлеA нарисованожирной линией.Это деревоизображаеткратчайшиймаршрут из узлаA до всехостальных узловв сети. Например, кратчайшиймаршрут из узлаA в узел Fпроходит черезузлы A, C,E, F.
Многиеалгоритмыпоиска кратчайшегомаршрута начинаютс пустого дерева, к которомузатем добавляетсяпо одной связидо тех пор, покадерево не будетзаполнено. Этиалгоритмы можноразбить на двекатегории всоответствиисо способомвыбора следующейсвязи, котораядолжна бытьдобавлена крастущемудереву кратчайшегомаршрута.
Алгоритмыустановкиметок (labelsetting) всегдавыбирают связь, которая гарантированноокажется частьюконечногократчайшегомаршрута. Этотметод работаетаналогичнометоду поисканаименьшегоостовногодерева. Еслисвязь добавленав дерево, тоона не будетудалена позже.
Алгоритмыкоррекцииметок (labelcorrecting) добавляютсвязи, которыемогут быть илине быть частьюконечногократчайшегомаршрута. Впроцессе рабыалгоритма онможет определить, что на местоуже находящейсяв дереве связинужно поместитьдругую связь.В этом случаеалгоритм заменяетстарую связьновой и продолжаетработу. Заменасвязи в деревеможет сделатьвозможнымипути, которыене были возможныдо этого. Чтобыпроверить этипути, алгоритмуприходитсяснова проверитьпути, которыебыли добавленыв дерево раньшеи использовалиудаленнуюсвязь.

=====324

@Рис.12.8. Дерево кратчайшегомаршрута

Алгоритмыустановки икоррекцииметок, описанныев следующихразделах, используютпохожие классыдля представленияузлов и связей.Класс узлавключает полеDist, которое определяетрасстояниеот корня доузла в растущемдереве кратчайшегомаршрута. Валгоритмеустановкиметок, послевставки узлав дерево полюDistприсваиваетсяправильноезначение, и онов дальнейшемне меняется.В алгоритмекоррекцииметок, значениеполя Distможет понадобитьсяобновить, еслиалгоритм заменитсвязь.
Классузла такжевключает полеNodeStatus, которое указывает, находится лиузел в деревекратчайшегомаршрута, спискевозможныхсвязей, или нив одной из этихструктур. ПолеInLinkуказывает насвязь, котораяведет к узлув дереве кратчайшегомаршрута.
    продолжение
--PAGE_BREAK--
PublicId As Integer
PublicX As Single
PublicY As Single
PublicLinks As Collection
PublicDist As Integer ' Расстояниеот корня деревапути.
PublicNodeStatus As Integer ' Статусдеревамаршрута.
PublicInLink As PathSLink ' Связь, ведущаяк узлу.

======325

Используяполе InLink, программа можетперечислитьузлы в пути откорня до узлаIв обратномпорядке припомощи следующегокода:

Dimnode As PathSNode

Setnode = I
Do
'Вывести узел.
Printnode.Id
Ifnode Is Root Then Exit Do

'Перейти к следующемуузлу вверх подереву.
Ifnode.IsLink.Node1 Is node Then
Setnode = node.InLink.Node2
Else
Setnode = node.InLink.Node1
EndIf
Loop

Классlinkв алгоритмевключает полеInPathTree, которое указывает, является лисвязь частьюдерева кратчайшегомаршрута.

PublicNode1 As PathSNode
PublicNode2 As PathSNode
PublicCost As Integer
PublicInPathTree As Boolean

Обаалгоритмаустановки икоррекции метокиспользуютсписок возможныхсвязей, в которомнаходятсясвязи, которыемогут бытьдобавлены вдерево кратчайшегомаршрута, ноони по разномуоперируют этимсписком. Алгоритмустановки метоквсегда выбираетсвязь, котораяобязательноокажется частьюдерева кратчайшегомаршрута. Алгоритмкоррекции метоквыбирает элемент, который находитсяна вершинесписка.Установкаметок
В началеэтого алгоритмазначения поляDistкорневого узлаустанавливаетсяравным 0. Затемкорневой узелпомещаетсяв список возможныхузлов, при этомзначение поляNodeStatusэтого узлапринимаетзначение NOW_IN_LIST, указывая нато, что он находитсяв списке.
Послеэтого выполняетсяпоиск в спискеузла с наименьшимзначением Dist.Первоначальноэто будет корневойузел, так какон единственныйв списке.
Затемалгоритм удаляетэтот узел изсписка, и устанавливаетзначение поляNodeStatusдля этого узларавным WAS_IN_LIST, указывая нато, что этотузел теперьявляется частьюдерева кратчайшегомаршрута. ПоляDistи IsLinkузла уже имеютправильныезначения. Длякаждого корневогоузла, значениеполя IsLinkравно Nothing, а значение поляDistравно нулю.
Послеэтого алгоритмпроверяет всесвязи, выходящиеиз выбранногоузла. Если соседнийузел на другомконце связиникогда ненаходился всписке возможныхузлов, то алгоритмдобавляет егок списку. Онустанавливаетзначение поляNodeStatusсоседнего узларавным NOW_IN_LIST., а значение поляDist —расстояниюот корневогоузла до выбранногоузла плюс ценесвязи. И, наконец, он присваиваетзначение полюInLinkсоседнего узлатак, чтобы оноуказывало насвязь с соседнимузлом.

========326

Во времяпроверки алгоритмомсвязей, выходящихиз выбранногоузла, если значениеполя NodeStatusсоседнего узларавно NOW_IN_LIST, то этот узелуже находитсяв списке возможныхузлов. Алгоритмпроверяеттекущее значениеDistсоседнего узла, проверяя, небудет ли путьчерез выбранныйузел короче.Если это так, то он обновляетполя InLinkи Distсоседнего узлаи оставляетсоседний узелв списке возможныхузлов.
Алгоритмповторяет этотпроцесс, удаляяузлы из спискавозможныхузлов, проверяясоседние с нимиузлы и добавляясоседние узлыв список до техпор, пока списокне опустеет.
На рис.12.9 показана частьдерева кратчайшегомаршрута. Вэтой точкеалгоритм проверилузлы A и B, удалил их изсписка возможныхузлов, и проверилих связи. УзлыA и B ужедобавлены кдереву кратчайшегомаршрута, итеперь в спискевозможных узловнаходятся узлыC, D и E.Жирные стрелкина рис. 12.9 соответствуютзначениям полейInLinkузлов в этойточке. Например, значение поляInLinkдля узла Eсоответствуетсвязи междуузлами Eи B.
Послеэтого алгоритмищет в спискевозможных узловузел с наименьшимзначением Dist.В данной точкезначения полейDistузлов C, Dи E равны10, 21 и 22 соответственно, поэтому алгоритмвыбирает узелC. Узел Cудаляется изсписка возможныхузлов, и егополю NodeStatusприсваиваетсязначениеWAS_IN_LIST.Теперь узелC являетсячастью деревакратчайшегомаршрута, и егополя Distи InLinkимеют правильныезначения.
Затемалгоритм проверяетсвязи, выходящиеиз узла C.Единственнаясвязь, выходящаяиз узла C, идет к узлу E, который ужесодержитсяв списке возможныхузлов, поэтомуалгоритм недобавляет егов список снова.
Текущийкратчайшиймаршрут откорня в узелE — это путьA, B, E, полная ценакоторого равна22. Но цена путиA, C, Eравна всего17., что меньше, чем текущаяцена 22, поэтомуалгоритм обновляетзначение InLinkдля узла E, и присваиваетполю Distэтого узлазначение 17.

@Рис.12.9. Часть деревакратчайшегомаршрута

=========327

PrivateSub FindPathTree(root As PathSNode)
Dimcandidates As New Collection
Dimi As Integer
Dimbest_i As Integer
Dimbest_dist As Integer
Dimnew_dist As Integer
Dimnode As PathSNode
Dimto_node As PathSNode
Dimlink As PathSLink

Ifroot Is Nothing Then Exit Sub

'Сброситьзначения полейMarked и NodeStatusвсех узлов,
'и флаги Usedи InPathTree всехсвязей.
ResetPathTree

'Начать с корнядерева кратчайшегомаршрута.
root.Dist= 0
Setroot.InLink = Nothing
root.NodeStatus= NOW_IN_LIST
candidates.Addroot

DoWhile candidates.Count > 0
'Найти ближайшийк корню узел кандидат.
best_dist= INFINITY
Fori = 1 To candidates.Count
new_dist= candidates(i).Dist
Ifnew_dist
best_i= i
best_dist= new_dist
EndIf
Nexti

'Добавить узелк дерева кратчайшегомаршрута.
Setnode = candidates(best_i)
candidates.Removebest_i
node.NodeStatus= WAS_IN_LIST
'Проверитьсоседние узлы.
ForEach link In node.Links
Ifnode Is link.Node1 Then
Setto_node = link.Node2
Else
Setto_node = link.Node1
EndIf
Ifto_node.NodeStatus = NOT_IN_LIST Then
'Узел раньшене был в спискевозможных
'узлов.Добавить егов список.
candidates.Addto_node
to_node.NodeStatus= NOW_IN_LIST
to_node.Dist= best_dist + link.Cost
Setto_node.InLink = link
ElseIfto_node.NodeStatus = NOW_IN_LIST Then
'Узел находитсяв списке возможныхузлов.
'Обновить значенияего полей Dist иinlink,
'если это необходимо.
new_dist= best_dist + link.Cost
Ifnew_dist
to_node.Dist= new_dist
Setto_node.InLink = link
EndIf
EndIf
Nextlink
Loop

GotPathTree= True
'Пометить входящиеузлы, чтобы ихбыло прощевывести наэкран.
ForEach node In Nodes
IfNot (node.InLink Is Nothing) Then _
node.InLink.InPathTree= True
Nextnode
'Перерисоватьсеть.
DrawNetwork
EndSub

Важно, чтобы алгоритмобновлял поляInLinkи Distтолько дляузлов, в которыхполе NodeStatusравно NOW_IN_LIST.Для большинствасетей нельзяполучить болеекороткий путь, добавляя узлы, которые ненаходятся всписке возможныхузлов. Тем неменее, еслисеть содержитцикл, полнаядлина которогоотрицательна, алгоритм можетобнаружить, что можно уменьшитьрасстояниедо некоторыхузлов, которыеуже находятсяв дереве кратчайшегомаршрута, приэтом две ветвидерева кратчайшегомаршрута окажутсясвязаннымидруг с другом, так что оноперестанетбыть деревом.
На рис.12.10 показана сетьс циклом отрицательнойцены и «дерево»кратчайшегомаршрута, котороеполучилосьбы, если бы алгоритмобновлял ценуузлов, которыеуже находятсяв дереве.

=======329

@Рис.12.10. Неправильное«дерево» кратчайшегомаршрута длясети с цикломотрицательнойцены

ПрограммаPathSиспользуетэтот алгоритмустановки метокдля вычислениякратчайшегомаршрута. ОнааналогичнапрограммамNetEditи Span.Если вы не вставляетеили не удаляетеузел или связь, то можно выбратьузел при помощимыши и программапри этом найдети выведет наэкран деревократчайшегомаршрута скорнем в этомузле. На рис.12.11 показано окнопрограммы PathSс деревом кратчайшегомаршрута скорнем в узле3.

@Рис.12.11. Дерево кратчайшегомаршрута скорнем в узле3

=======330
Вариантыметода установкиметок
Узкоеместо этогоалгоритмазаключаетсяв поиске узлас наименьшимзначением поляDistв списке возможныхузлов. Некоторыеварианты этогоалгоритмаиспользуютдругие структурыданных дляхранения спискавозможныхузлов. Например, можно было быиспользоватьупорядоченныйсвязный список.При использованииэтого методапотребуетсятолько одиншаг для того, чтобы найтиследующий узел, который будетдобавлен кдереву кратчайшегомаршрута. Этотсписок будетвсегда упорядоченным, поэтому узелна вершинесписка всегдабудет искомымузлом.
Этооблегчит поискнужного узлав списке, ноусложнит добавлениеузла в него.Вместо тогочтобы простопомещать узелв начало списка, его придетсяпоместить внужную позицию.
Иногдатакже требуетсяперемещатьузлы в списке.Если в результатедобавленияузла в деревократчайшегомаршрута уменьшилоськратчайшеерасстояниедо другогоузла, которыйуже был в списке, то нужно переместитьэтот элементближе к вершинесписка.
Предыдущийалгоритм и этотего новый вариантпредставляютсобой два крайнихслучая управлениясписком возможныхузлов. Первыйалгоритм совсемне упорядочиваетсписок и тратитдостаточномного временина поиск узловв сети. Второйтратит многовремени наподдержаниеупорядоченностисписка, но можеточень быстровыбирать изнего узлы. Другиеварианты используютпромежуточныестратегии.
Например, можно использоватьдля хранениясписка возможныхузлов приоритетнуюочередь наоснове пирамид, тогда можнобудет простовыбрать следующийузел с вершиныпирамиды. Вставканового узлав пирамиду иее переупорядочениебудет выполнятьсябыстрее, чеманалогичныеоперации дляупорядоченногосвязного списка.Другие стратегиииспользуютсложные схемыорганизацииблоков длятого, чтобыупростить поисквозможныхузлов.
Некоторыеиз этих вариантовдостаточносложны. Из заэтой их сложностиэти алгоритмыдля небольшихсетей частовыполняютсямедленнее, чемболее простыеалгоритмы. Темне менее, дляочень большихсетей или сетей, в которых каждыйузел имееточень большоечисло связей, выигрыш отпримененияэтих алгоритмовможет стоитьдополнительногоусложнения.Коррекцияметок
Как иалгоритм установкиметок, этоталгоритм начинаетс обнулениязначения поляDistкорневого узлаи помещаеткорневой узелв список возможныхузлов. При этомзначения полейDistостальных узловустанавливаютсяравными бесконечности.Затем для вставкив дерево кратчайшегомаршрута выбираетсяпервый узелв списке возможныхузлов.
Послеэтого алгоритмпроверяет узлы, соседние свыбранным, выясняя, будетли расстояниеот корня довыбранногоузла плюс ценасвязи меньше, чем текущеезначение поляDistсоседнего узла.Если это так, то поля Distи InLinkсоседнего узлаобновляютсятак, чтобы кратчайшиймаршрут к соседнемуузлу проходилчерез выбранныйузел. Если соседнийузел при этомне находилсяв списке возможныхузлов, то алгоритмтакже добавляетего к списку.Заметьте, чтоалгоритм непроверяет, попадал ли этотузел в списокраньше. Еслипуть от корнядо соседнегоузла становитсякороче, узелвсегда добавляетсяв список возможныхузлов.
Алгоритмпродолжаетудалять узлыиз списка возможныхузлов, проверяясоседние с нимиузлы и добавляясоседние узлыв список до техпор, пока списокне опустеет.
Есливнимательносравнить алгоритмыустановки метоки коррекцииметок, то видно, что они похожи.Единственноеотличие заключаетсяв том, как каждыйиз них выбираетэлементы изсписка возможныхузлов для вставкив дерево кратчайшегомаршрута.

=====331

Алгоритмустановки метоквсегда выбираетсвязь, котораягарантированнонаходится вдереве кратчайшегомаршрута. Приэтом послетого, как узелудаляется изсписка возможныхузлов, он навсегдапомещаетсяв дерево и большене попадаетв список возможныхузлов.
Алгоритмкорректировкивсегда выбираетпервый узелиз списка возможныхузлов, которыйне всегда можетбыть наилучшимвыбором. Значенияполей Distи InLinkэтого узламогут быть ненаилучшимииз возможных.В этом случаеалгоритм, вконце концов, найдет в спискеузел, черезкоторый проходитболее короткийпуть к выбранномуузлу. Тогдаалгоритм обновляетполя Distи InLinkи снова помещаетобновленныйузел в списоквозможныхузлов.
Алгоритмможет использоватьновый путь длясоздания другихпутей, которыеон мог пропуститьраньше. Помещаяобновленныйузел снова всписок обновленныхузлов, алгоритмгарантирует, что этот узелбудет проверенснова и будутнайдены всетакие пути.

PrivateSub FindPathTree(root As PathCNode)
Dimcandidates As New Collection
Dimnode_dist As Integer
Dimnew_dist As Integer
Dimnode As PathCNode
Dimto_node As PathCNode
Dimlink As PathCLink

Ifroot Is Nothing Then Exit Sub

'Сброситьполя Marked иNodeStatus для всехузлов,
'и флагиUsed и InPathTree длявсех связей.
ResetPathTree

'Начать с корнядерева кратчайшегомаршрута.
root.Dist= 0
Setroot.InLink = Nothing
root.NodeStatus= NOW_IN_LIST
candidates.Addroot

DoWhile candidates.Count > 0
'Добавить узелв дерево кратчайшегомаршрута.
Setnode = candidates(1)
candidates.Remove1
node_dist= node.Dist
node.NodeStatus= NOT_IN_LIST

'Проверитьсоседние узлы.
ForEach link In node.Links
Ifnode Is link.Node1 Then
Setto_node = link.Node2
Else
Setto_node = link.Node1
EndIf

'Проверить, существуетли более короткий
'путь через этотузел.
new_dist= node_dist + link.Cost
Ifto_node.Dist > new_dist Then
'Путь лучше.Обновить значенияDist и InLink.
Setto_node.InLink = link
to_node.Dist= new_dist
'Добавить узелв список возможныхузлов,
'если его тамеще нет.
Ifto_node.NodeStatus = NOT_IN_LIST Then
candidates.Addto_node
to_node.NodeStatus= NOW_IN_LIST
EndIf
EndIf
Nextlink
Loop
'Пометить входящиесвязи, чтобыих было прощевывести.
ForEach node In Nodes
IfNot (node.InLink Is Nothing) Then _
node.InLink.InPathTree= True
Nextnode
'Перерисоватьсеть.
DrawNetwork
EndSub

В отличиеот алгоритмаустановкиметок, этоталгоритм неможет работатьс сетями, которыесодержат циклыс отрицательнойценой. Есливстречаетсятакой цикл, тоалгоритм бесконечноперемещаетсяпо связям внутринего. При каждомобходе цикларасстояниедо входящихв него узловуменьшается, при этом алгоритмснова помещаетузлы в списоквозможныхузлов, и сноваможет проверятьих в дальнейшем.При следующейпроверке этихузлов, расстояниедо них такжеуменьшится, и так далее.Этот процессбудет продолжатьсядо тех пор, покарасстояниедо этих узловне достигнетнижнего граничногозначения -32.768, если длина путизадана целымчислом. Еслиизвестно, чтов сети имеютсяциклы с отрицательнойценой, то прощевсего простоиспользоватьдля работы сней метод установки, а не коррекцииметок.
ПрограммаPathCиспользуетэтот алгоритмкоррекции метокдля вычислениякратчайшегомаршрута. ОнааналогичнапрограммеPathS, но используетметод коррекции, а не установкиметок.

=======333
Вариантыметода коррекцииметок
Алгоритмкоррекции метокпозволяет оченьбыстро выбратьузел из спискавозможныхузлов. Он такжеможет вставитьузел в списоквсего за одинили два шага.Недостатокэтого алгоритмазаключаетсяв том, что когдаон выбираетузел из спискавозможныхузлов, он можетсделать неслишком хорошийвыбор. Еслиалгоритм выбираетузел до того, как его поляDistи InLinkполучат своиконечный значения, он должен позднеескорректироватьзначения этихполей и сновапоместить узелв список возможныхузлов. Чем чащеалгоритм помещаетузлы назад всписок возможныхузлов, тем большевремени этозанимает.
Вариантыэтого алгоритмапытаются повыситькачество выбораузлов без большогоусложненияалгоритма. Одиниз методов, который неплохоработает напрактике, состоитв том, чтобыдобавлять узлыодновременнов начало и конецсписка возможныхузлов. Еслиузел раньшене попадал всписок возможныхузлов, алгоритм, как обычно, добавляет егов конец списка.Если узел ужебыл раньше всписке возможныхузлов, но сейчасего там нет, алгоритм вставляетего в началосписка. Приэтом повторноеобращение кузлу выполняетсяпрактическисразу, возможнопри следующемже обращениик списку.
Идея, заключеннаяв таком подходе, состоит в том, чтобы еслиалгоритм совершаетошибку, онаисправляласькак можно быстрее.Если ошибкане будет исправленав течение достаточнодолгого времени, алгоритм можетиспользоватьнеправильнуюинформациюдля построениядлинных ложныхпутей, которыезатем придетсяисправлять.Благодарябыстрому исправлениюошибок, алгоритмможет уменьшитьчисло неверныхпутей, которыепридется перестроить.В наилучшемслучае, есливсе соседниеузлы все ещенаходятся всписке возможныхузлов, повторнаяпроверка этогоузла до проверкисоседей предотвратитпостроениеневерных путей.Другиезадачи поискакратчайшегомаршрута
Описанныевыше алгоритмыпоиска кратчайшегомаршрута вычисляливсе кратчайшиепути из корневогоузла до всехостальных узловв сети. Существуетмножестводругих типовзадачи нахождениякратчайшегомаршрута. Вэтом разделеобсуждаютсятри из них: двухточечныйкратчайшиймаршрут(point to pointshortest path), кратчайшиймаршрут длявсех пар(allpairs shortestpath) и кратчайшиймаршрут соштрафами заповороты.Двухточечныйкратчайшиймаршрут
В некоторыхприложенияхможет понадобитьсянайти кратчайшиймаршрут междудвумя точками, при этом остальныепути в полномдереве кратчайшегомаршрута неважны. Простойспособ решитьэту задачу —вычислитьполное деревократчайшегомаршрута припомощи методаустановки иликоррекцииметок, а затемвыбрать издерева кратчайшийпуть междудвумя точками.
Другойспособ заключаетсяв использованииметода установкиметок, которыйостанавливалсябы, когда будетнайден путьк конечномуузлу. Алгоритмустановки метокдобавляет кдереву кратчайшегомаршрута толькоте пути, которыеобязательнодолжны в немнаходиться, следовательно, в тот момент, когда алгоритмдобавит конечныйузел в дерево, будет найденискомый кратчайшиймаршрут. В алгоритме, который обсуждалсяраньше, этопроисходит, когда алгоритмудаляет конечныйузел из спискавозможныхузлов.
    продолжение
--PAGE_BREAK--
=======334

Единственноеизменениетребуетсявнести в частьалгоритмаустановкиметок, котораявыполняетсясразу же послетого, как алгоритмнаходит в спискевозможных узловузел с наименьшимзначением Dist.Перед удалениемузла из спискавозможныхузлов, алгоритмдолжен проверить, не являетсяли этот узелискомым. Еслиэто так, то деревократчайшегомаршрута ужесодержит кратчайшиймаршрут междуначальным иконечным узлами, и алгоритмможет закончитьработу.

'Найти ближайшийк корню узелв списке возможныхузлов.
:

'Проверить, является лиэтот узел искомым.
Ifnode = destination Then Exit Do

'Добавить этотузел в деревократчайшегомаршрута.
:

На практике, если две точкив сети расположеныдалеко другот друга, тоэтот алгоритмобычно будетвыполнятьсядольше, чемзаймет вычислениеполного деревакратчайшегомаршрута. Алгоритмвыполняетсямедленнее из затого, что в каждомцикле выполненияалгоритмапроверяется, достигнут лиискомый узел.С другой стороны, если узлы расположенырядом, то выполнениеэтого алгоритмаможет потребоватьнамного меньшевремени, чемпостроениеполного деревакратчайшегомаршрута.
Длянекоторыхсетей, такихкак сеть улиц, можно оценить, насколькоблизко расположеныдве точки, изатем решить, какую версиюалгоритмавыбрать. Еслисеть содержитвсе улицы южнойКалифорнии, и две точкирасположенына расстоянии10 миль, следуетиспользоватьверсию, котораяостанавливаетсяпосле того, какнайдет конечныйузел. Если жеточки удаленыдруг от другана 100 миль, возможно, меньше временизаймет вычислениеполного деревакратчайшегомаршрута.Вычислениекратчайшегомаршрута длявсех пар
В некоторыхприложенияхможет потребоватьсябыстро найтикратчайшиймаршрут междувсеми парамиузлов в сети.Если нужновычислитьбольшую частьиз N2 возможныхпутей, можетбыть быстреевычислить всевозможные путивместо того, чтобы находитьтолько те, которыенужны.
Можнозаписать кратчайшиемаршруты, используядва двумерныхмассива, Distи InLinks.В ячейке Dist(I,J)находитсякратчайшиймаршрут из узлаIв узел J, а в ячейке InLinks(I,J) —связь, котораяведет к узлуJв кратчайшемпути из узлаIв узел J.Эти значенияаналогичнызначениям Distи InLinkв классе узлав предыдущемалгоритме.
Одиниз способовнайти все кратчайшиемаршруты заключаетсяв том, чтобыпостроитьдеревья кратчайшегомаршрута скорнем в каждомиз узлов сетипри помощиодного из предыдущихалгоритмов, и затем сохранитьрезультатыв массивахDistsи InLinks.

========335

Другойметод вычислениявсех кратчайшихмаршрутовпоследовательностроит пути, используя всебольше и большеузлов. Вначалеалгоритм находитвсе кратчайшиемаршруты, которыеиспользуюттолько первыйузел и узлы наконцах пути.Другими словами, для узлов Jи Kалгоритм находиткратчайшиймаршрут междуэтими узлами, который используеттолько узелс номером 1 иузлы Jи K, если такой путьсуществует
Затемалгоритм находитвсе кратчайшиемаршруты, которыеиспользуюттолько двапервых узла.Затем он строитпути, используяпервые триузла, первыечетыре узла, и так далее дотех пор, покане будут построенывсе кратчайшиемаршруты, используявсе узлы. В этотмомент, посколькукратчайшиемаршруты могутиспользоватьлюбой узел, алгоритм найдетвсе кратчайшиемаршруты всети.
Заметьте, что кратчайшиймаршрут междуузлами Jи K, использующийтолько первыеI узлов, включает узелI, только еслиDist(J,K)> Dist(J,I)+ Dist(I,K).Иначе кратчайшиммаршрутом будетпредыдущийкратчайшиймаршрут, которыйиспользовалтолько первыеI- 1 узлов.Это означает, что когда алгоритмрассматриваетузел I, требуетсятолько проверитьвыполнениеусловия Dist(J,K)> Dist(J,I)+ Dist(I,K).Если это условиевыполняется, алгоритм обновляеткратчайшиймаршрут из узлаJв узел K.Иначе старыйкратчайшиймаршрут междуэтими двумяузлами осталсябы таковым.Штрафыза повороты
В некоторыхсетях, в особенностисетях улиц, бывает полезнодобавить штрафи запреты наповороты (turnpenalties) В сетиулиц автомобильдолжен замедлитьдвижение передтем, как выполнитьповорот. Поворотналево можетзанимать большевремени, чемповорот направоили движениепрямо. Некоторыеповороты могутбыть запрещеныили невозможныиз за наличияразделительнойполосы. Этиаспекты можноучесть, вводяв сеть штрафыза повороты.Небольшоечисло штрафовза повороты
Частоважны тольконекоторыештрафы за повороты.Может понадобитьсяпредотвратитьвыполнениезапрещенныхили невозможныхповоротов иприсвоитьштрафы за поворотылишь на несколькихключевыхперекрестках, не определяяштрафы для всехперекрестковв сети. В этомслучае можноразбить каждыйузел, для которогозаданы штрафы, на несколькоузлов, которыебудут неявноучитыватьштрафы.
Предположим, что требуетсядобавить одинштраф за поворотна перекресткеналево и другойштраф за поворотнаправо. Нарис. 12.12 показанперекресток, на которомтребуетсяприменить этиштрафы. Числорядом с каждойсвязью соответствуетее цене. Требуетсяприменитьштрафы за входв узел A посвязи L1, и затем выходиз него по связямL2 или L3.
Дляпримененияштрафов к узлуA, разобьемэтот узел надва узла, поодному длякаждой из покидающихего связей. Вданном примере, из узла Aвыходят двесвязи, поэтомуузел A разбиваетсяна два узла A1и A2, и связи, выходящие изузла A, заменяютсясоответствующимисвязями, выходящимииз полученныхузлов. Можнопредставить, что каждый издвух образовавшихсяузлов соответствуетвходу в узелA и поворотув сторонусоответствующейсвязи.

======336

@Рис.12.12. Перекресток

Затемсвязь L1, входящая в узелA, заменяетсяна две связи, входящие вкаждый из двухузлов A1и A2. Ценаэтих связейравна ценеисходной связиL1 плюсштрафу за поворотв соответствующемнаправлении.На рис. 12.13 показанперекресток, на которомвведены штрафыза поворот. Наэтом рисункештраф за поворотналево из узлаA равен 5, аза поворотнаправо —2.
Помещаяинформациюо штрафахнепосредственнов конфигурациюсети, мы избегаемнеобходимостимодифицироватьалгоритмыпоиска кратчайшегомаршрута. Этиалгоритмы будутнаходить правильныекратчайшиемаршруты сучетом штрафовза повороты.
Приэтом придетсявсе же слегкаизменить программы, чтобы учестьразбиение узловна несколькочастей. Предположим, что требуетсянайти кратчайшиймаршрут междуузлами Iи J, но узелI оказалсяразбит на несколькоузлов. Полагая, что можно покинутьузел I полюбой связи, можно создатьложный узели использоватьего в качествекорня деревакратчайшегомаршрута. Соединимэтот узел связямис нулевой ценойс каждым изузлов, получившихсяпосле разбиенияузла I. Тогда, если построитьдерево кратчайшегомаршрута скорнем в ложномузле, то приэтом будутнайдены всекратчайшиемаршруты, содержащиелюбой из этихузлов. На рис.12.14 показан перекрестокс рис. 12.13, связанныйс ложным корневымузлом.

@Рис.12.13. Перекрестоксо штрафамиза повороты

=======337

@Рис.12.14. Перекресток, связанный сложным корнем

Обрабатыватьслучай поискапути к узлу, который былразбит на несколькоузлов, проще.Если требуетсянайти кратчайшиймаршрут междуузлами Iи J, и узелJ был разбитна несколькоузлов, то вначале, как обычно, нужно найтидерево кратчайшегомаршрута скорнем в узлеI. Затемпроверяютсявсе узлы, накоторые былразбит узелJ и находитсяближайший изних к корнюдерева. Путьк этому узлуи есть кратчайшиймаршрут к исходномуузлу J.Большоечисло штрафовза повороты
Предыдущийметод будетне слишкомэффективным, если вы хотитеввести штрафыза поворотыдля большинстваузлов в сети.Лучше будетсоздать совершенноновую сеть, которая будетвключать информациюо штрафах.
Для каждой связи между узлами A и B в исходной сети в новой сети создается узел AB;
Если в исходной сети соответствующие связи были соединены, то полученные узлы также соединяются между собой. Например, предположим, что в исходной сети одна связь соединяла узлы A и B, а другая — узлы B и C. Тогда в новой сети нужно создать связь, соединяющую узел AB с узлом BC;
Цена новой связи складывается из цены второй связи в исходной сети и штрафа за поворот. В этом примере цена связи между узлом AB и узлом BC будет равна цене связи, соединяющей узлы B и C в исходной сети плюс штрафу за поворот при движении из узла A в узел B и затем в узел C.
На рис.12.15 изображенанебольшая сетьи соответствующаяновая сеть, представляющаяштрафы за повороты.Штраф за поворотналево равен3, за поворотнаправо — 2, аза «поворот»прямо — нулю.Например, таккак поворотиз узла Bв узел E —это левый поворотв исходнойсети, штраф длясвязи междуузлами BEи EF в новойсети равен 3.Цена связи, соединяющейузлы E и Fв исходнойсети, равна 3, поэтому полнаяцена новойсвязи равна3 + 3 = 6.

=======338

@Рис.12.15. Сеть и соответствующаяей сеть со штрафамиза повороты

Предположимтеперь, чтотребуется найтидля исходнойсети деревократчайшегомаршрута скорнем в узлеD. Чтобысделать это, создадим вновой сетиложный корневойузел, затемпостроим связи, соединяющиеэтот узел совсеми связями, которые покидаютузел D висходной сети.Присвоим этимсвязям ту жецену, которуюимеют соответствующиесвязи в исходнойсети. На рис.12.16 показана новаясеть с рис. 12.15 сложным корневымузлом, соответствующимузлу D. Деревократчайшегомаршрута в этойсети нарисованожирной линией.
Чтобынайти кратчайшиймаршрут из узлаD в узел C, необходимопроверить всеузлы в новойсети, которыесоответствуютсвязям, заканчивающимсяв узле C. Вэтом примереэто узлы BCи FC. Ближайшийк ложному корнюузел соответствуеткратчайшемумаршруту к узлуC в исходнойсети. Узлы вкратчайшеммаршруте вновой сетисоответствуютсвязям в кратчайшеммаршруте висходной сети.

@Рис.12.16. Дерево кратчайшегомаршрута в сетисо штрафамиза повороты

========339

На рис.12.16 кратчайшиймаршрут начинаетсяс ложного корня, идет в узел DE, затем узлы EFи FC и имеетполную цену16. Этот путьсоответствуетпути D, E,F, C висходной сети.Прибавив одинштраф за левыйповорот E,F, C, получим, что цена этогопути в исходнойсети такжеравна 16.
Заметьте, что вы не нашлибы этот путь, если бы построилидерево кратчайшегомаршрута висходной сети.Без учета штрафовза повороты, кратчайшиммаршрутом изузла D в узелC был бы путьD, E, B,C с полнойценой 12. С учетомштрафов ценаэтого путиравна 17.Примененияметода поискакратчайшегомаршрута
Вычислениякратчайшегомаршрута используютсяво многихприложениях.Очевиднымпримером являетсяпоиск кратчайшегомаршрута междудвумя точкамив уличной сети.Многие другиеприложенияиспользуютметод поискакратчайшегомаршрута менееочевиднымиспособами.Следующиеразделы описываютнекоторые изэтих приложений.Разбиениена районы
Предположим, что имеетсякарта города, на которуюнанесены всепожарные депо.Может потребоватьсяопределитьдля каждойточки городаближайшее кней депо. Напервый взглядэто кажетсятрудной задачей.Можно попытатьсярассчитатьдерево кратчайшегомаршрута скорнем в каждомузле сети, чтобынайти, какоедепо расположеноближе всегок каждому изузлов. Или можнопостроитьдерево кратчайшегомаршрута скорнем в каждомиз пожарныхдепо и записатьрасстояниеот каждого изузлов до каждогоиз депо. Носуществуетнамного болеебыстрый метод.
Создадимложный корневойузел и соединимего с каждымиз пожарныхдепо связямис нулевой ценой.Затем найдемдерево кратчайшегомаршрута скорнем в этомложном узле.Для каждойточки в сетикратчайшиймаршрут изложного корневогоузла к этойточке пройдетчерез ближайшеек этой точкепожарное депо.Чтобы найтиближайшее кточке пожарноедепо, нужнопросто проследоватьпо кратчайшемумаршруту отэтой точки ккорню, пока напути не встретитсяодно из депо.Построив всегоодно деревократчайшегомаршрута, можнонайти ближайшиепожарные деподля каждойточки в сети.
ПрограммаDistrictиспользуетэтот алгоритмдля разбиениясети на районы.Так же, как ипрограмма PathCи другие программы, описанные вэтой главе, онапозволяетзагружать, редактироватьи сохранятьна диске ориентированныесети с ценойсвязей. Есливы не добавляетеи не удаляетеузлы или связи, вы можете выбратьдепо для разделенияна районы. Добавьтеузлы к спискупожарных депощелчком левойкнопки мыши, затем щелкнитеправой кнопкойв любом местеформы, и программаразобьет сетьна районы.
На рис.12.17 показано окнопрограммы, накотором изображенасеть с тремядепо. Депо вузлах 3, 18 и 20 обведеныжирными кружочками.Разбивающиесеть на районыдеревья кратчайшегомаршрута изображеныжирными линиями.

=====340

@Рис.12.17. ПрограммаDistrict
Составлениеплана работс использованиемметода критическогопути
Во многихзадачах, в томчисле в большихпрограммныхпроектах, определенныедействия должныбыть выполненыраньше других.Например, пристроительстведома до установкифундаментанужно вырытькотлован, фундаментдолжен застытьдо того, какначнется возведениестен, каркасдома долженбыть собранпрежде, чемможно будетвыполнятьпроводкуэлектричества, водопроводаи кровельныеработы и такдалее.
Некоторыеиз этих задачмогут выполнятьсяодновременно, другие должнывыполнятьсяпоследовательно.Например, можноодновременнопроводитьэлектричествои прокладыватьводопровод.
Критическимпутем (criticalpath) называетсяодна из самыхдлинных последовательностейзадач, котораядолжна бытьвыполнена длязавершенияпроекта. Важностьзадач, лежащихна критическомпути, определяетсятем, что сдвигсроков выполненияэтих задачприведет кизменениювремени завершенияпроекта в целом.Если заложитьфундамент нанеделю позже, то и зданиебудет завершенона неделю позже.Для определениязаданий, которыенаходятся накритическомпути, можноиспользоватьмодифицированныйалгоритм поискакратчайшегомаршрута.
Вначалесоздадим сеть, которая представляетвременныесоотношениямежду задачамипроекта. Пустькаждой задачесоответствуетузел. Нарисуемсвязь междузадачей Iи задачей J, если задачаI должнабыть выполненадо начала задачиJ, и присвоимэтой связицену, равнуювремени выполнениязадачи I.
Послеэтого создадимдва ложныхузла, один изкоторых будетсоответствоватьначалу проекта, а другой — егозавершению.Соединим начальныйузел связямис нулевой ценойсо всеми узламив проекте, вкоторые невходит ни однадругая связь.Эти узлы соответствуютзадачам, выполнениекоторых можноначинать немедленно, не ожидая завершениядругих задач.
Затемсоздадим ложныесвязи нулевойдлины, соединяющиевсе узлы, изкоторых невыходит неодной связи, с конечнымузлом. Эти узлыпредставляютзадачи, которыене тормозятвыполнениедругих задач.После того, каквсе эти задачибудут выполнены, проект будетзавершен.
Найдясамый длинныймаршрут междуначальным иконечным узламисети, мы получимкритическийпуть проекта.Входящие в негозадачи будуткритичнымидля выполненияпроекта.

========341

@Таблица12.1. Этапы сборкидождевальнойустановки

Рассмотрим, например, упрощенныйпроект сборкидождевальнойустановки, состоящий изпяти задач. Втабл. 12.1 приведенызадачи и временныесоотношениямежду ними.Сеть для этогопроекта показанана рис. 12.18.
В этомпростом примерелегко увидеть, что самый длинныймаршрут в сетивыполняетследующуюпоследовательностьзадач: выкопатьканавы, смонтироватьтрубы, закопатьих. Это критическиезадачи, и еслив выполнениикакой либоиз них наступитзадержка, выполнениепроекта такжезадержится.
Длинаэтого критическогопути равнаожидаемомувремени завершенияпроекта. В данномслучае, есливсе задачибудут выполненывовремя, выполнениепроекта займетпять дней. Приэтом предполагаетсятакже, что еслиэто возможно, несколько задачбудут выполнятьсяодновременно.Например, одинчеловек можеткопать канавы, пока другойбудет закупатьтрубы.
В болеезначительномпроекте, такомкак строительствонебоскребаили съемкафильма, могутсодержатьсятысячи задач, и критическиепути при этоммогут бытьсовсем не очевидны.Планированиеколлективнойработы
Предположим, что требуетсянабрать несколькосотрудниковдля ответовна телефонныезвонки, приэтом каждыйиз них будетзанят не весьдень. При этомнужно, чтобысуммарнаязарплата быланаименьшей, и нанятый коллективсотрудниковотвечал назвонки с 9 утрадо 5 вечера. Втабл. 12.2 приведенырабочие часысотрудников, и их почасоваяоплата.

@Рис.12.18. Сеть задачсборки дождевальнойустановки

======342

@Таблица12.2. Рабочие часысотрудникови их почасоваяоплата

Дляпостроениясоответствующейсети, создадимодин узел длякаждого рабочегочаса. Соединимэти узлы связями, каждая из которыхсоответствуетрабочим часамкакого либосотрудника.Если сотрудникможет работатьс 9 до 11, нарисуемсвязь междуузлом 9:00 и узлом11:00, и присвоимэтой связицену, равнуюзарплате, получаемойданным сотрудникомза соответствующеевремя. Еслисотрудникполучает 6,5 долларовв час, и отрезоквремени составляетдва часа, тоцена связиравна 13 долларам.На рис. 12.19 показанасеть, соответствующаяданным из табл.12.2.
Кратчайшиймаршрут изпервого узлав последнийпозволяетнабрать коллективсотрудниковс наименьшейсуммарнойзарплатой.Каждая связьв пути соответствуетработе сотрудникав определенныйпромежутоквремени. В данномслучае кратчайшиймаршрут из узла9:00 в узел 5:00 проходитчерез узлы11:00, 12:00 и 3:00. Этомусоответствуетследующийграфик работы: сотрудник Aработает с 9:00до 11:00, сотрудникD работаетс 11:00 до 12:00, затемсотрудник Aснова работаетс 12:00 до 3:00 и сотрудникE работаетс 3:00 до 5:00. Полнаязарплата всехсотрудниковпри таком графикесоставляет52,15 доллара.
    продолжение
--PAGE_BREAK--
@Рис.12.19. Сеть графикаработы коллектива

======343
Максимальныйпоток
Во многихсетях связиимеют кромецены, еще ипропускнуюспособность(capacity). Черезкаждый узелсети можетпроходитьпоток (flow), который непревышает еепропускнойспособности.Например, поулицам можетпроехать толькоопределеннойчисло машин.Сеть с заданнымипропускнымиспособностямиее связей называетсянагруженнойсетью (capacitatednetwork). Еслизадана нагруженнаясеть, задачао максимальномпотоке заключаетсяв определениинаибольшеговозможногопотока черезсеть из заданногоисточника(source) в заданныйсток (sink).
На рис.12.20 показананебольшаянагруженнаясеть. Числарядом со связямив этой сети —это не ценасвязи, а еепропускнаяспособность.В этом примеремаксимальныйпоток, равный4, получается, если две единицыпотока направляютсяпо пути A,B, E,Fи еще две — попути A, C,D, F.
Описанныйздесь алгоритмначинаетсяс того, что потокво всех связяхравен нулю изатем алгоритмпостепенноувеличиваетпоток, пытаясьулучшить найденноерешение. Алгоритмзавершаетработу, еслинельзя улучшитьимеющеесярешение.
Дляпоиска путейспособов увеличенияполного потока, алгоритм проверяетостаточнуюпропускнуюспособность(residual capacity)связей. Остаточнаяпропускнаяспособностьсвязи междуузлами Iи J равнамаксимальномудополнительномупотоку, которыйможно направитьиз узла Iв узел J, используя связьмежду I иJ и связьмежду J иI. Этот суммарныйпоток можетвключатьдополнительныйпоток по связиI J, еслив этой связиесть резервпропускнойспособности, или исключатьчасть потокаиз связи J I, если по этойсвязи идетпоток.
Например, предположим, что в сети, соединяющейузлы A и Cна рис. 12.20, существуетпоток, равный2. Так как пропускнаяспособностьэтой связиравна 3, то к этойсвязи можнодобавить единицупотока, поэтомуостаточнаяпропускнаяспособностьэтой связиравна 1. Хотясеть, показаннаяна рис. 12.20 не имеетсвязи C A, для этой связисуществуетостаточнаяпропускнаяспособность.В данном примере, так как по связиA C идетпоток, равный2, то можно удалитьдо двух единицэтого потока.При этом суммарныйпоток из узлаC в узел Aувеличилсябы на 2, поэтомуостаточнаяпропускнаяспособностьсвязи C Aравна 2.

@Рис.12.20. Нагруженнаясеть

========344

@Рис.12.21. Потоки в сети

Сеть, состоящая извсех связейс положительнойостаточнойпропускнойспособностью, называетсяостаточнойсетью (residualnetwork). На рис.12.21 показана сетьс рис. 12.20, каждойсвязи в которойприсвоен поток.Для каждойсвязи, первоечисло равнопотоку черезсвязь, а второе —ее пропускнойспособности.Надпись «1/2», например, означает, что поток черезсвязь равен1, и ее пропускнаяспособностьравна 2. Связи, поток черезкоторые большенуля, нарисованыжирными линиями.
На рис.12.22 показанаостаточнаясеть, соответствующаяпотокам на рис.12.21. Нарисованытолько связи, которые действительномогут иметьостаточнуюпропускнуюспособность.Например, междуузлами Aи D не нарисованони одной связи.Исходная сетьне содержитсвязи A Dили D A, поэтому этисвязи всегдабудут иметьнулевую остаточнуюпропускнуюспособность.
Одноиз свойствостаточныхсетей состоитв том, что любойпуть, использующийсвязи с остаточнойпропускнойспособностьюбольше нуля, который связываетисточник состоком, даетспособ увеличенияпотока в сети.Так как этотпуть дает способувеличенияили расширенияпотока в сети, он называетсярасширяющимпутем (augmentingpath). На рис.12.23 показанаостаточнаясеть с рис. 12.22 срасширяющимпутем, нарисованнымжирной линией.
Чтобыобновить решение, используярасширяющийпуть, найдемнаименьшуюостаточнуюпропускнуюспособностьв пути. Затемскорректируемпотоки в путив соответствиис этим значением.Например, нарис. 12.23 наименьшаяостаточнаяпропускнаяспособностьсетей в расширяющемпути равна 2.Чтобы обновитьпотоки в сети, к любой связиI J напути добавляетсяпоток 2, а из всехобратных имсвязей J Iвычитаетсяпоток 2.

@Рис.12.22. Остаточнаясеть

========345

@Рис.12.23. Расширяющийпуть черезостаточнуюсеть

Вместотого, чтобыкорректироватьпотоки, и затемперестраиватьостаточнуюсеть, прощепросто скорректироватьостаточнуюсеть. Затемпосле завершенияработы алгоритмаможно использоватьрезультат длявычисленияпотоков длясвязей в исходнойсети.
Чтобыскорректироватьостаточнуюсеть в этомпримере, проследуемпо расширяющемупути. Вычтем2 из остаточнойпропускнойспособностивсех связейI J вдольпути, и добавим2 к остаточнойпропускнойспособностисоответствующейсвязи J I.На рис. 12.24 показанаскорректированнаяостаточнаясеть для этогопримера.
Еслибольше нельзянайти ни одногорасширяющегопути, то можноиспользоватьостаточнуюсеть для вычисленияпотоков в исходнойсети. Для каждойсвязи междуузлами Iи J, еслиостаточныйпоток междуузлами Iи J меньше, чем пропускнаяспособностьсвязи, то потокдолжен равнятьсяпропускнойспособностиминус остаточныйпоток. В противномслучае потокдолжен бытьравен нулю.
Например, на рис. 12.24 остаточныйпоток из узлаA в узел Cравен 1 и пропускнаяспособностьсвязи A Cравна 3. Так как1 меньше 3, то потокчерез узелбудет равен3 — 1 = 2. На рис. 12.25 показаныпотоки в сети, соответствующиеостаточнойсети на рис.12.24.

@Рис.12.24. Скорректированнаяостаточнаясеть

========346

@Рис.12.25. Максимальныепотоки

Полученныйалгоритм ещене содержитметода дляпоиска расширяющихпутей в остаточнойсети. Один извозможныхметодов аналогиченметоду коррекцииметок для алгоритмакратчайшегомаршрута. Вначалепоместимузел источникв список возможныхузлов. Затем, если списоквозможных узловне пуст, будемудалять из негопо одному узлу.Проверим всесоседние узлы, соединенныес выбраннымузлом по связи, остаточнаяпропускнаяспособностькоторой большенуля. Если соседнийузел еще не былпомещен в списоквозможныхузлов, добавитьего в список.Продолжитьэтот процессдо тех пор, покасписок возможныхузлов не опустеет.
Этотметод имеетдва отличияот метода поискакратчайшегомаршрута коррекциейметок. Во первых, этот метод непрослеживаетсвязи с нулевойостаточнойпропускнойспособностью.Алгоритм жекратчайшегомаршрута проверяетвсе пути, независимоот их цены.
Во вторых, этот алгоритмпроверяет всеузлы не большеодного раза.Алгоритм поискакратчайшегомаршрута коррекциейметок, будетобновлять узлыи помещать ихснова в списоквозможныхузлов, если онпозднее найдетболее короткийпуть от корняк этому узлу.При поискерасширяющегопути нет необходимостипроверять егодлину, поэтомуне нужно обновлятьпути и помещатьузлы назад всписок возможныхузлов.
Следующийкод демонстрирует, как можно вычислятьмаксимальныепотоки в программена Visual Basic.Этот код предназначендля работы снеориентированнымисетями, похожимина те, которыеиспользовалисьв других программахпримеров, описанныхв этой главе.После завершенияработы алгоритмаон присваиваетсвязи цену, равную потокучерез нее, взятомусо знаком минус, если потоктечет в обратномнаправлении.Другими словами, если сеть содержитобъект, представляющийсвязь I J, а алгоритмопределяет, что поток должентечь в направлениисвязи J I, то потоку черезсвязь I Jприсваиваетсязначение, равноепотоку, которыйдолжен был бытечь черезсвязь J I, взятому сознаком минус.Это позволяетпрограммеопределятьнаправлениепотока, используясуществующуюструктуруузлов.

=======347

PrivateSub FindMaxFlows()
Dimcandidates As Collection

DimResidual() As Integer
Dimnum_nodes As Integer
Dimid1 As Integer
Dimid2 As Integer
Dimnode As FlowNode
Dimto_node As FlowNode
Dimfrom_node As FlowNode
Dimlink As FlowLink
Dimmin_residual As Integer

IfSourceNode Is Nothing Or SinkNode Is Nothing _
ThenExit Sub
'Задать размермассива остаточнойпропускнойспособности.
num_nodes= Nodes.Count
ReDimResidual(1 To num_nodes, 1 To num_nodes)

'Первоначальнозначения остаточнойпропускнойспособности
'равны значениямпропускнойспособности.
ForEach node In Nodes
id1= node.Id
ForEach link In node.Links
Iflink.Node1 Is node Then
Setto_node = link.Node2
Else
Setto_node = link.Node1
EndIf
id2= to_node.Id
Residual(id1,id2) = link.Capacity
Nextlink
Nextnode

'Повторять дотех пор, покабольше
'не найдетсярасширяющихпутей.
Do
'Найти расширяющийпуть в остаточнойсети.
'Сбросить значенияNodeStatus и InLink всех узлов.
ForEach node In Nodes
node.NodeStatus= NOT_IN_LIST
Setnode.InLink = Nothing
Nextnode

'Начать с пустогосписка возможныхузлов.
Setcandidates = New Collection
'Поместитьисточник всписок возможныхузлов.
candidates.AddSourceNode
SourceNode.NodeStatus= NOW_IN_LIST
'Продолжать, пока списоквозможных узловне опустеет.
DoWhile candidates.Count > 0
Setnode = candidates(1)
candidates.Remove1
node.NodeStatus= WAS_IN_LIST
id1= node.Id
'Проверитьвыходящие изузла связи.
ForEach link In node.Links
Iflink.Node1 Is node Then
Setto_node = link.Node2
Else
Setto_node = link.Node1
EndIf
id2= to_node.Id

'Проверить, чтоresidual > 0, и этот узел
'никогда не былв списке.
IfResidual(id1, id2) > 0 And _
to_node.NodeStatus= NOT_IN_LIST _
Then
'Добавить узелв список.
candidates.Addto_node
to_node.NodeStatus= NOW_IN_LIST
Setto_node.InLink = link
EndIf
Nextlink

'Остановиться, если помеченузел сток.
IfNot (SinkNode.InLink Is Nothing) Then _
ExitDo
Loop

'Остановиться, если расширяющийпуть не найден.
IfSinkNode.InLink Is Nothing Then Exit Do

'Найти наименьшуюостаточнуюпропускнуюспособность
'вдоль расширяющегопути.
min_residual= INFINITY
Setnode = SinkNode
Do
Ifnode Is SourceNode Then Exit Do
id2= node.Id
Setlink = node.InLink
Iflink.Node1 Is node Then
Setfrom_node = link.Node2
Else
Setfrom_node = link.Node1
EndIf
id1= from_node.Id

Ifmin_residual > Residual(id1, id2) Then _
min_residual= Residual(id1, id2)
Setnode = from_node
Loop

'Обновить остаточныепропускныеспособности,
'используярасширяющийпуть.
Setnode = SinkNode
Do
Ifnode Is SourceNode Then Exit Do
id2= node.Id

Setlink = node.InLink
Iflink.Node1 Is node Then
Setfrom_node = link.Node2
Else
Setfrom_node = link.Node1
EndIf
id1= from_node.Id

Residual(id1,id2) = Residual(id1, id2) _
— min_residual
Residual(id2,id1) = Residual(id2, id1) _
+min_residual
Setnode = from_node
Loop
Loop' Повторять, пока большене останетсярасширяющихпутей.

'Вычислитьпотоки в остаточнойсети.
ForEach link In Links
id1= link.Node1.Id
id2= link.Node2.Id
Iflink.Capacity > Residual(id1, id2) Then
link.Flow= link.Capacity — Residual(id1, id2)
Else
'Отрицательныезначениясоответствуют
'обратномунаправлениюдвижения.
link.Flow= Residual(id2, id1) — link.Capacity
EndIf
Nextlink
'Найти полныйпоток.
TotalFlow= 0
ForEach link In SourceNode.Links
TotalFlow= TotalFlow + Abs(link.Flow)
Nextlink
EndSub

=======348-350

ПрограммаFlowиспользуетметод поискарасширяющегопути для нахождениямаксимальногопотока в сети.Она похожа наостальныепрограммы вэтой главе.Если вы не добавляетеили не удаляетеузел или связь, вы можете выбратьисточник припомощи левойкнопки мыши, а затем выбратьсток при помощиправой кнопкимыши. Послевыбора источникаи стока программавычисляет ивыводит наэкран максимальныйпоток. На рис.12.26 показано окнопрограммы, накотором изображеныпотоки в небольшойсети.Приложениямаксимальногопотока
Вычислениямаксимальногопотока используютсяво многихприложениях.Хотя для многихсетей можетбыть важнознать максимальныйпоток, этотметод частоиспользуетсядля получениярезультатов, которые напервый взглядимеют отдаленноеотношение кпропускнойспособностисети.Непересекающиесяпути
Большиесети связидолжны обладатьизбыточностью(redundancy). Длязаданной сети, например такой, как на рис. 12.27, может потребоватьсянайти числонепересекающихсяпутей из источникак стоку. Приэтом, если междудвумя узламисети есть множествонепересекающихсяпутей, все связив которых различны, то соединениемежду этимиузлами останется, даже если несколькосвязей в сетибудут разорваны.
Можноопределитьчисло различныхпутей, используяметод вычислениямаксимальногопотока. Создадимсеть с узламии связями, соответствующимиузлам и связямв коммуникационнойсети. Присвоимкаждой связиединичнуюпропускнуюспособность.

@Рис.12.26. ПрограммаFlow

=====351

@Рис.12.27. Сеть коммуникаций

Затемвычислим максимальныйпоток в сети.Максимальныйпоток будетравен числуразличных путейот источникак стоку. Таккак каждаясвязь можетнести единичныйпоток, то ниодин из путей, использованныхпри вычислениимаксимальногопотока, не можетиметь общейсвязи.
Приболее строгомопределенииизбыточностиможно потребовать, чтобы различныепути не имелини общих связей, ни общих узлов.Немного изменивпредыдущуюсеть, можноиспользоватьвычислениемаксимальногопотока длярешения и этойзадачи.
Разделимкаждый узелза исключениемисточника истока на дваузла, соединенныхсвязью единичнойпропускнойспособности.Соединим первыйиз полученныхузлов со всемисвязями, входящимив исходныйузел. Все связи, выходящие изисходного узла, присоединимко второмуполученномупосле разбиенияузлу. На рис.12.28 показана сетьс рис. 12.27, узлы накоторой разбитытаким образом.Теперь найдеммаксимальныйпоток для этойсети.
Еслипуть, использованныйдля вычислениямаксимальногопотока, проходитчерез узел, тоон может использоватьсвязь, котораясоединяет дваполучившихсяпосле разбиенияузла. Так какэта связь имеетединичнуюпропускнуюспособность, никакие двапути, полученныепри вычислениимаксимальногопотока, не могутпройти по этойсвязи междуузлами, поэтомув исходной сетиникакие двапути не могутиспользоватьодин и тот жеузел.

@Рис.12.28. Коммуникационнаясеть послепреобразования

======352

@Рис.12.29. Сеть распределенияработы
Распределениеработы
Предположим, что имеетсягруппа сотрудников, каждый из которыхобладаетопределенныминавыками. Предположимтакже, что существуетряд заданий, которые требуютпривлечениясотрудника, обладающегозаданным наборомнавыков. Задачараспределенияработы (workassignment) состоитв том, чтобыраспределитьработу междусотрудникамитак, чтобы каждоезадание выполнялсотрудник, имеющий соответствующиенавыки.
Чтобысвести этузадачу к вычислениюмаксимальногопотока, создадимсеть с двумястолбцамиузлов. Каждыйузел в левомстолбце представляетодного сотрудника.Каждый узелв правом столбцепредставляетодно задание.
Затемсравним навыкикаждого сотрудникас навыками, необходимымидля выполнениякаждого иззаданий. Создадимсвязь междукаждым сотрудникоми каждым заданием, которое онспособен выполнить, и присвоим всемсвязям единичнуюпропускнуюспособность.
Создадимузел источники соединим егос каждым изсотрудниковсвязью единичнойпропускнойспособности.Затем создадимузел сток исоединим с нимкаждое задание, снова при помощисвязей с единичнойпропускнойспособностью.На рис. 12.29 показанасоответствующаясеть для задачираспределенияработы с четырьмясотрудникамии четырьмязаданиями.
Теперьнайдем максимальныйпоток из источникав сток. Каждаяединица потокадолжна пройтичерез один узелсотрудникаи один узелзадания. Этотпоток представляетраспределениеработы дляэтого сотрудника.

@Рис.12.30. ПрограммаWork

=======353

Еслисотрудникиобладаютсоответствующиминавыками длявыполнениявсех заданий, то вычислениямаксимальногопотока распределятих все. Еслиневозможновыполнить всезадания, то впроцессе вычислениямаксимальногопотока работабудет распределенатак, чтобы быловыполненомаксимальновозможное числозаданий.
ПрограммаWorkиспользуетэтот алгоритмдля распределенияработы междусотрудниками.Введите фамилиисотрудникови их навыки втекстовом полеслева, а задания, которые требуетсявыполнить итребующиесядля них навыкив текстовомполе посередине.После того, каквы нажмете накнопку Go(Начать), программараспределитработу междусотрудниками, используя дляэтого сетьмаксимальногопотока. На рис.12.30 показано окнопрограммы сполученнымраспределениемработы.Резюме
Некоторыесетевые алгоритмыможно применитьнепосредственнок сетеподобнымобъектам. Например, можно использоватьалгоритм поискакратчайшегомаршрута длянахождениянаилучшегопути в уличнойсети. Для определениянаименьшейстоимостипостроениясети связи илисоединениягородов железнымидорогами можноиспользоватьминимальноеостовное дерево.
Многиедругие сетевыеалгоритм находятменее очевидныеприменения.Например, можноиспользоватьалгоритмыпоиска кратчайшегомаршрута дляразбиения нарайоны, составленияплана работметодом кратчайшегопути, или графикаколлективнойработы. Алгоритмывычислениямаксимальногопотока можноиспользоватьдля распределенияработы. Этименее очевидныеприменениясетевых алгоритмовобычно оказываютсяболее интереснымии перспективными.
    продолжение
--PAGE_BREAK--
======354
Глава13. Объектно ориентированныеметоды
Использованиефункций и подпрограммпозволяетпрограммистуразбить кодбольшой программына части. Массивыи определенныепользователемтипы данныхпозволяютсгруппироватьэлементы данныхтак, чтобы упроситьработу с ними.
Классы, которые впервыепоявились в4-й версии VisualBasic, позволяютпрограммиступо новомусгруппироватьданные и логикуработы программы.Класс позволяетобъединитьв одном объектеданные и методыработы с ними.Этот новыйподход к управлениюсложностьюпрограмм позволяетвзглянуть наалгоритмы сдругой точкизрения.
В этойглаве рассматриваютсявопросыобъектно ориентированногопрограммирования, возникающиепри примененииклассов VisualBasic. В нейописаны преимуществаобъектно ориентированногопрограммирования(ООП) и показано, какую выгодуможно получитьот их примененияв программахна языке VisualBasic. Затемв главе рассматриваетсянабор полезныхобъектно ориентированныхпримеров, которыевы можетеиспользоватьдля управлениясложностьюваших приложений.ПреимуществаООП
К традиционнымпреимуществамобъектно ориентированногопрограммированияотносятсяинкапсуляцияили скрытие(encapsulation), полиморфизм(polymorphism) и повторноеиспользование(reuse). Реализацияих в классахVisual Basicнесколькоотличаетсяот того, какони реализованыв другихобъектно ориентированныхязыках. В следующихразделахрассматриваютсяэти преимуществаООП и то, какможно имивоспользоватьсяв программахна Visual Basic.Инкапсуляция
Объект, определенныйпри помощикласса, заключаетв себе данные, которые онсодержит. Другиечасти программымогут использоватьобъект дляоперированияего данными, не зная о том, как хранятсяили изменяютсязначения данных.Объект предоставляетоткрытые (public)процедуры, функции, и процедурыизменениясвойств, которыепозволяютпрограммекосвенноманипулироватьили просматриватьданные. Так какпри этом данныеявляются абстрактнымис точки зренияпрограммы, этотакже называетсяабстракциейданных (dataabstraction).
Инкапсуляцияпозволяетпрограммеиспользоватьобъекты как«черные ящики».Программа можетиспользоватьоткрытые методыобъекта дляпроверки иизменениязначений безнеобходимостиразбиратьсяв том, что происходитвнутри черногоящика.

=========355

Посколькудействия внутриобъектов скрытыот основнойпрограммы, реализацияобъекта можетменяться безизмененияосновной программы.Изменения всвойствахобъекта происходяттолько в модулекласса.
Например, предположим, что имеетсякласс FileDownload, который скачиваетфайлы из Internet.Программасообщает классуFileDownloadположениеобъекта, а объектвозвращаетстроку с содержимымфайла. В этомслучае программене требуетсязнать, какимобразом объектпроизводитзагрузку файла.Он может скачиватьфайл, используямодемное соединениеили соединениепо выделеннойлинии, или дажеизвлекать файлиз кэша на локальномдиске. Программазнает только, что объектвозвращаетстроку послетого, как емупередаетсяссылка на файл.Обеспечениеинкапсуляции
Дляобеспеченияинкапсуляциикласс долженпредотвращатьнепосредственныйдоступ к своимданным. Еслипеременнаяв классе объявленакак открытая, то другие частипрограммысмогут напрямуюизменять исчитыватьданные из нее.Если позднеепредставлениеданных изменится, то любые частипрограммы, которые непосредственновзаимодействуютс данными, такжедолжны будутизмениться.При этом теряетсяпреимуществоинкапсуляции.
Чтобыобеспечитьдоступ к данным, класс должениспользоватьпроцедуры дляработы со свойствами.Например, следующиепроцедурыпозволяютдругим частямпрограммыпросматриватьи изменятьзначение DegreesFобъекта Temperature.

Privatem_DegreesF As Single ' ГрадусыФаренгейта.

PublicProperty Get DegreesF() As Single
DegreesF= m_DegreesF
EndProperty

PublicProperty Let DegreesF(new_DegreesF As Single)
m_DegreesF= new_DegreesF
EndProperty

Различиямежду этимипроцедурамии определениемm_DegreesFкак открытойпеременнойпока невелики.Тем не менее, использованиеэтих процедурпозволяет легкоизменять классв дальнейшем.Например, предположим, что вы решитеизмерять температурув градусахКельвина, а неФаренгейта.При этом можноизменить класс, не затрагиваяостальныхчастей программы, в которыхиспользуютсяпроцедурысвойства DegreesF.Можно такжедобавить коддля проверкиошибок, чтобыубедиться, чтопрограмма непопытаетсяпередать объектунедопустимыезначения.

Privatem_DegreesK As Single ' ГрадусыКельвина.

PublicProperty Get DegreesF() As Single
DegreesF= (m_DegreesK — 273.15) * 1.8
EndProperty

PublicProperty Let DegreesF(ByVal new_DegreesF As Single)
Dimnew_value As Single

new_value= (new_DegreesF / 1.8) + 273.15
Ifnew_value
'Сообщить обошибке   недопустимоезначении.
Error.Raise380, «Temperature», _
«Температурадолжна бытьнеотрицательной.»
Else
m_DegreesK= new_value
EndIf
EndProperty

======357

Программы, описанные вэтом материале, безобразнонарушают принципинкапсуляции, используя вклассах открытыепеременные.Это не слишкомхороший стильпрограммирования, но так сделанопо трем причинами.
Во первых, непосредственноеизменениезначений данныхвыполняетсябыстрее, чемвызов процедурсвойств. Большинствопрограмм ужеи так несколькотеряют в производительностииз за использованияссылок на объектывместо примененияболее сложногометода псевдоуказателей.Примененияпроцедур свойствеще сильнеезамедлит ихработу.
Во вторых, многие программыдемонстрируютметоды работысо структурамиданных. Например, сетевые алгоритмы, описанные в12 главе, непосредственноиспользуютданные объекта.Указатели, которые связываютузлы в сетидруг с другом, составляютнеотъемлемуючасть алгоритмов.Было бы бессмысленноменять способхранения этихуказателей.
И, наконец, благодаряиспользованиюоткрытых значенийданных, кодстановитсяпроще. Это позволяетвам сконцентрироватьсяна алгоритмах, и этому не мешаютлишние процедурыработы со свойствами.Полиморфизм
Второепреимуществообъектно ориентированногопрограммирования —это полиморфизм(polymorphism), чтоозначает «имеющиймножествоформ». В VisualBasic это означает, что один объектможет иметьразличный формыв зависимостиот ситуации.Например, следующийкод представляетсобой подпрограмму, которая можетпринимать вкачестве параметралюбой объект.Объект objможет бытьформой, элементомуправления, или объектомопределенноговами класса.

PrivateSub ShowName(obj As Object)
MsgBoxTypeName(obj)
EndSub

Полиморфизмпозволяетсоздаватьпроцедуры, которые могутработать буквальносо всеми типамиобъектов. Ноза эту гибкостьприходитсяплатить. Еслиопределитьобобщенный(generic)объект, как вэтом примере, то Visual Basicне сможет определить, какие типыдействий сможетвыполнятьобъект, до запускапрограммы.

========357

ЕслиVisual Basicзаранее знает, с объектомкакого типаон будет иметьдело, он можетвыполнитьпредварительныедействия длятого, чтобыболее эффективноиспользоватьобъект. Еслииспользуетсяобобщенный(generic)объект, то программане может выполнитьподготовки, и в результатеэтого потеряетв производительности.
ПрограммаGenericдемонстрируетразницу впроизводительностимежду объявлениемобъектов какпринадлежащихк определенномутипу или какобобщенныхобъектов. Тествыполняетсяодинаково, заисключениемтого, что в одномиз случаевобъект определяется, как имеющийтип Object, а не тип SpecificClass.При этом установказначения данныхобъекта сиспользованиемобобщенногообъекта выполняетсяв 200 раз медленнее.

PrivateSub TestSpecific()
ConstREPS = 1000000 ' Выполнитьмиллион повторений.

Dimobj As SpecificClass
Dimi As Long
Dimstart_time As Single
Dimstop_time As Single

Setobj = New SpecificClass
start_time= Timer
Fori = 1 To REPS
obj.Value= I
Nexti
stop_time= Timer
SpecificLabel.Caption= _
Format$(1000* (stop_time — start_time) / REPS, «0.0000»)
EndSub
Зарезервированноеслово Implements
В 5 йверсии VisualBasic зарезервированноеслово Implements(Реализует)позволяетпрограммеиспользоватьполиморфизмбез использованияобобщенныхобъектов. Например, программа можетопределитьинтерфейсVehicle(Средствопередвижения), Если классыCar(Автомобиль)и Truck(Грузовик) обареализуютинтерфейсVehicle, то программаможет использоватьдля выполненияфункций интерфейсаVehicleобъекты любогоиз двух классов.
Создадимвначале классинтерфейса, в котором определимоткрытые переменные, которые онбудет поддерживать.В нем такжедолжны бытьопределеныпрототипыоткрытых процедурдля всех методов, которые онбудет поддерживать.Например, следующийкод демонстрирует, как класс Vehicleможет определитьпеременнуюSpeed(Скорость) иметод Drive(Вести машину):

PublicSpeed Long

PublicSub Drive()

EndSub

=======358

Теперьсоздадим класс, который реализуетинтерфейс.После оператораOptionExplicitв секции Declaresдобавляетсяоператор Implementsопределяющийимя классаинтерфейса.Этот классдолжен такжеопределятьвсе необходимыедля работылокальныепеременные.
КлассCarреализуетинтерфейсVehicle.Следующий коддемонстрирует, как в нем определяетсяинтерфейс изакрытая (private)переменнаяm_Speed:

OptionExplicit

ImplementsVehicle

Privatem_Speed As Long

Когдак классу добавляетсяоператор Implements,Visual Basicсчитываетинтерфейс, определенныйуказаннымклассом, а затемсоздает соответствующиезаглушки в кодекласса. В этомпримере VisualBasic добавитновую секциюVehicleв исходный кодкласса Car, и определитпроцедуры letи getсвойстваVehicle_Speedдля представленияпеременнойSpeed, определеннойв интерфейсеVehicle.В процедуреletVisual BasicиспользуетпеременнуюRHS, которая являетсясокращениемот RightHandSide(С правой стороны), в которой задаетсяновое значениепеременной.
ТакжеопределяетсяпроцедураVehicle_Drive.Чтобы реализоватьфункции этихпроцедур, нужнонаписать коддля них. Следующийкод демонстрирует, как класс Carможет определятьпроцедуры Speedи Drive.

PrivateProperty Let Vehicle_Speed(ByVal RHS As Long)
m_Speed= RHS
EndProperty

PrivateProperty Get Vehicle_Speed() As Long
Vehicle_Speed= m_Speed
EndProperty

PrivateSub Get Vehicle_Drive()
'Выполнитькакие то действия.
:
EndProperty

Послетого, как интерфейсопределен иреализованв одном илинесколькихклассах, программаможет полиморфноиспользоватьэлементы в этихклассах. Например, допустим, чтопрограммаопределилаклассы Carи Track, которые обареализуютинтерфейсVehicle.Следующий коддемонстрирует, как программаможет проинициализироватьзначения переменнойSpeedдля объектаCarи объекта Truck.

Dimobj As Vehicle

Setobj = New Car
obj.Speed= 55
Setobj = New Truck
obj.Speed =45

==========359

Ссылкаobjможет указыватьлибо на объектCar, либо на объектTruck.Так как в обоихэтих объектахреализованинтерфейсVehicle, то программаможет оперироватьсвойствомobj.Speedнезависимоот того, указываетли ссылка objна Carили Truck.
Таккак ссылка objуказывает наобъект, которыйреализуетинтерфейсVehicle, то Visual Basicзнает, что этотобъект имеетпроцедуры, работающиесо свойствомSpeed.Это означает, что он можетвыполнятьвызовы процедурсвойства Speedболее эффективно, чем это былобы в случае, если бы objбыла ссылкойна обобщенныйобъект.
ПрограммаImplemявляется доработаннойверсией программыописанной вышепрограммыGeneric.Она сравниваетскорость установкизначений сиспользованиемобобщенныхобъектов, определенныхобъектов иобъектов, которыереализуютинтерфейс. Водном из тестовна компьютерес процессоромPentium с тактовойчастотой 166 МГц, программепотребовалось0,0007 секунды дляустановкизначений прииспользованииопределенноготипа объекта.Для установкизначений прииспользованииобъекта, реализующегоинтерфейс, потребовалось0,0028 секунды (в 4раза больше).Для установкизначений прииспользованииобобщенногообъекта потребовалось0,0508 секунды (в 72раза больше).Использованиеинтерфейсаявляется нетаким быстрым, как использованиессылки наопределенныйобъект, но намногобыстрее, чемиспользованиеобобщенныхобъектов.Наследованиеи повторноеиспользование
Процедурыи функцииподдерживаютповторноеиспользование(reuse). Вместотого, чтобыкаждый разписать кодзаново, можнопоместить егов подпрограмму, тогда вместоблока кодаможно простоподставитьвызов подпрограммы.
Аналогично, определениепроцедуры вклассе делаетее доступнойво всей программе.Программа можетиспользоватьэту процедуру, используяобъект, которыйявляется экземпляромкласса.
В средепрограммистов, использующихобъектно ориентированныйподход, подповторнымиспользованиемобычно подразумеваетсянечто большее, а именно наследование(inheritance). Вобъектно ориентированныхязыках, такихкак C++ илиDelphi, один классможет порождать(derive) другой.При этом второйкласс наследует(inherits) всюфункциональностьпервого класса.После этогоможно добавлять, изменять илиубирать какие либофункции изкласса наследника.Это также являетсяформой повторногоиспользованиякода, посколькупри этом программистуне нужно зановореализоватьфункции родительскогокласса, длятого, чтобыиспользоватьих в классе наследнике.
ХотяVisual Basic ине поддерживаетнаследованиенепосредственно, можно добитьсяпримерно техже результатов, используяограничение(containment) илиделегирование(delegation).При делегированииобъект из одногокласса содержитэкземпляркласса из другогообъекта, и затемпередает частьсвоих обязанностейзаключенномув нем объекту.
Например, предположим, что имеетсякласс Employee, который представляетданные о сотрудниках, такие как фамилия, идентификационныйномер в системесоциальногострахованияи зарплата.Предположим, что нам теперьнужен классManager, который делаетто же самое, что и классEmployee, но имеет ещеодно свойствоsecretary(секретарь).
Дляиспользованияделегирования, класс Managerдолжен включатьв себя закрытыйобъект типаEmployeeс именем m_Employee.Вместо прямоговычислениязначений, процедурыработы со свойствамифамилии, номерасоциальногострахованияи зарплатыпередаютсоответствующиевызовы объектуm_Employee.Следующий коддемонстрирует, как класс Managerможет оперироватьпроцедурамисвойства name(фамилия):

==========360

Privatem_Employee As New Employee

PropertyGet Name() As String
Name= m_Employee.Name
EndProperty

PropertyLet Name (New_Name As String)
m_Employee.Name= New_Name
EndProperty

КлассManagerтакже можетизменять результат, возвращаемыйделегированнойфункцией, иливыдавать результатсама. Например, в следующемкоде показано, как класс Employeeвозвращаетстроку текстас данными осотруднике.

PublicFunction TextValues() As String
Dimtxt As String

txt= m_Name & vbCrLf
txt= txt & " " & m_SSN & vbCrLf
txt= txt & " " & Format$(m_Salary, «Currency»)& vbCrLf
TextValues= txt
EndFunction

КлассManagerиспользуетфункцию TextValuesобъекта Employee, но добавляетперед возвратоминформациюо секретарев строку результата.

PublicFunction TextValues() As String
Dimtxt As String
txt= m_Employee.TextValues
txt= txt & " " & m_Secretary & vbCrLf
TextValues= txt
EndFunction

ПрограммаInheritдемонстрируетклассы Employeeи Manager.Интерфейспрограммы непредставляетинтереса, ноее код включаетпростые определенияклассов Employeeи Manager.ПарадигмыООП
В первойглаве мы далиопределениеалгоритма как«последовательностиинструкцийдля выполнениякакого либозадания». Несомненно, класс можетиспользоватьалгоритмы всвоих процедурахи функциях.Например, можноиспользоватькласс для упаковкив него алгоритма.Некоторые изпрограмм, описанныхв предыдущихглавах, используютклассы дляинкапсуляциисложных алгоритмов.

=========361

Классытакже позволяютиспользоватьновый стильпрограммирования, при которомнесколькообъектов могутработать совместнодля выполнениязадачи. В этомслучае можетбыть бессмысленнымзадание последовательностиинструкцийдля выполнениязадачи. Болееадекватнымможет бытьзадание моделиповеденияобъектов, чемсведение задачик последовательностишагов. Для тогочтобы отличатьтакое поведениеот традиционныхалгоритмов, мы назовем их«парадигмами».
Следующиераздела описываютнекоторыеполезныеобъектно ориентированныепарадигмы.Многие из нихведут началоиз другихобъектно ориентированныхязыков, такихкак C++ илиSmalltalk, хотя онимогут такжеиспользоватьсяв Visual Basic.Управляющиеобъекты
Управляющиеобъекты (command)также называютсяобъектамидействия (actionobjects), функций(function objects)или функторами(functors). Управляющийобъект представляеткакое либодействие. Программаможет использоватьметод Execute(Выполнить) длявыполненияобъектом этогодействия. Программене нужно знатьничего об этомдействии, оназнает только, что объектимеет методExecute.
Управляющиеобъекты могутиметь множествоинтересныхприменений.Программа можетиспользоватьуправляющийобъект дляреализации:
Настраиваемых элементов интерфейса;
Макрокоманд;
Ведения и восстановления записей;
Функций «отмена» и «повтор».
Чтобысоздать настраиваемыйинтерфейс, форма можетсодержатьуправляющиймассив кнопок.Во время выполненияпрограммы формаможет загрузитьнадписи накнопках и создатьсоответствующийнабор управляющихобъектов. Когдапользовательнажимает накнопку, обработчикусобытий кнопкинужно всеголишь вызватьметод Executeсоответствующегоуправляющегообъекта. Деталипроисходящегонаходятсявнутри классауправляющегообъекта, а нев обработчикесобытий.
ПрограммаCommand1 используетуправляющиеобъекты длясозданиянастраиваемогоинтерфейсадля несколькихне связанныхмежду собойфункций. Принажатии накнопку программавызывает методExecuteсоответствующегоуправляющегообъекта.
Программаможет использоватьуправляющиеобъекты длясоздания определенныхпользователеммакрокоманд.Пользовательзадает последовательностьдействий, которыепрограммазапоминаетв коллекциив виде управляющихобъектов. Когдазатем пользовательвызываетмакрокоманду, программавызывает методыExecuteобъектов, которыенаходятся вколлекции.
Управляющиеобъекты могутобеспечиватьведение ивосстановлениезаписей. Управляющийобъект можетпри каждомсвоем вызовезаписыватьинформациюо себе в лог файл.Если программааварийно завершитработы, онаможет затемиспользоватьзаписаннуюинформациюдля восстановленияуправляющихобъектов ивыполненияих для повторенияпоследовательностикоманд, котораявыполняласьдо сбоя программы.
И, наконец, программа можетиспользоватьнабор управляющихобъектов дляреализациифункций отмены(undo) и повтора(redo).
=========362
    продолжение
--PAGE_BREAK--
===============13

Алгоритмпирамидальнойсортировки, также описанныйв 9 главе, произвольнопереходит отодной частисписка к другой.Для очень большихсписков этоможет приводитьк перегрузкепамяти. С другойстороны, сортировкаслиянием требуетбольшего объемапамяти, чемпирамидальнаясортировка.Если списокдостаточнобольшой, этотакже можетприводить кобращению кфайлу подкачки.Псевдоуказатели, ссылки на объектыи коллекции
В некоторыхязыках, напримерв C, C++ или Delphi, можно определятьпеременные, которые являютсяуказателями(pointers) на участкипамяти. В этихучастках могутсодержатьсямассивы, строки, или другиеструктурыданных. Частоуказательссылается наструктуру, которая содержитдругой указательи так далее.Используяструктуры, содержащиеуказатели, можно организовыватьвсевозможныесписки, графы, сети и деревья.В последующихглавах рассматриваютсянекоторые изэтих сложныхструктур.
До третьейверсии VisualBasic не содержалсредств дляпрямого созданияссылок. Тем неменее, посколькууказатель всеголишь ссылаетсяна какой либоучасток данных, то можно, создавмассив, использоватьцелочисленныйиндекс массивав качествеуказателя наего элементы.Это называетсяпсевдоуказателем(fake pointer).Ссылки
В 4-й версииVisual Basic быливпервые введеныклассы. Переменная, указывающаяна экземпляркласса, являетсяссылкой наобъект. Например, в следующемфрагменте кодапеременнаяobj —это ссылка наобъект классаMyClass.Эта переменнаяне указываетни на какойобъект, покаона не определяетсяпри помощизарезервированногослова New.Во второй строкеоператор Newсоздает новыйобъект и записываетссылку на негов переменнуюobj.

Dimobj As MyClass

Setobj = New MyClass

Ссылкив Visual Basic —это разновидностьуказателей.
Объектыв Visual Basicиспользуютсчетчик ссылок(reference counter)для упрощенияработы с объектами.Когда создаетсяновая ссылкана объект, счетчикссылок увеличиваетсяна единицу.После того, какссылка перестаетуказывать наобъект, счетчикссылок соответственноуменьшается.Когда счетчикссылок становитсяравным нулю, объект становитсянедоступнымпрограмме. Вэтот моментVisual Basicуничтожаетобъект и возвращаетзанятую импамять.
В следующихглавах болееподробно обсуждаютсяссылки и счетчикиссылок.Коллекции
Кромеобъектов иссылок, в 4-й версииVisual Basicтакже появилиськоллекции.Коллекцию можнопредставитькак разновидностьмассива. Они

================14

предоставляютв распоряжениепрограммистаудобные возможности, например можноменять размерколлекции, атакже осуществлятьпоиск объектапо ключу.Вопросыпроизводительности
Псевдоуказатели, ссылки и коллекцииупоминаютсяв этой главепотому, что онимогут сильновлиять напроизводительностьпрограммы.Ссылки и коллекциимогут упрощатьпрограммированиеопределенныхопераций, ноони могут потребоватьдополнительныхрасходов памяти.
ПрограммаFakerна диске с примерамидемонстрируетвзаимосвязьмежду псевдоуказателями, ссылками иколлекциями.Когда вы вводитечисло и нажимаетекнопку CreateList (Создатьсписок), программасоздает списокэлементов однимиз трех способов.Вначале онасоздает объекты, соответствующиеотдельнымэлементам, идобавляетссылки на объектык коллекции.Затем она используетссылки внутрисамих объектовдля созданиясвязанногосписка объектов.И, наконец, онасоздает связныйсписок припомощи псевдоуказателей.Пока не будемостанавливатьсяна том, как работаютсвязные списки.Они будут подробноразбиратьсяво 2 главе.
Посленажатия накнопку SearchList (Поискв списке), программаFakerвыполняет поискпо всем элементамсписка, а посленажатия накнопку DestroyList (Уничтожитьсписок) уничтожаетвсе списки иосвобождаетпамять.
В табл.1.5 приведенызначения времени, которое требуетсяпрограмме длявыполненияэтих задач накомпьютерес процессоромPentium с тактовойчастотой 90 МГц.Из таблицывидно, что заудобство работыс коллекциямиприходитсяплатить ценойбольшего времени, затрачиваемогона созданиеи уничтожениеколлекций.
Коллекциитакже содержатиндекс списка.Часть времени, затрачиваемогопри созданииколлекции, иуходит на созданиеиндекса. Приуничтоженииколлекциисохраняемыев ней ссылкиосвобождаются.При этом системапроверяет иобновляетсчетчики ссылокдля всех объектов.Если они равнынулю, то самобъект такжеуничтожается.Все это занимаетдополнительноевремя.
Прииспользованиипсевдоуказателейсоздание иуничтожениесписка происходиттак быстро, чтоэтим временемможно практическипренебречь.Системе приэтом не надозаботитьсяо ссылках, счетчикахссылок и обосвобожденииобъектов.
С другойстороны, поискв коллекцииосуществляетсягораздо быстрее, чем в двух остальныхслучаях, посколькуколлекцияиспользуетбыстрое хеширование(hashing) построенногоиндекса, в товремя как списокссылок и списокпсевдоуказателейиспользуютмедленныйпоследовательныйпоиск. В 11 главеобъясняется, как можно добавитьхешированиек своей программебез использованияколлекций.

@Таблица1.5. Время Создания/Поиска/Уничтожениясписков в секундах

==============15

Хотяприменениепсевдоуказателейобычно обеспечиваетлучшую производительность, оно менее удобно, чем использованиессылок. Еслив программенужен лишьнебольшойсписок, ссылкии коллекциимогут работатьдостаточнобыстро. Приработе с большимисписками можнополучить болеевысокую производительность, используяпсевдоуказатели.Резюме
Анализпроизводительностиалгоритмовпозволяетсравнить разныеалгоритмы. Онтакже помогаетоценить поведениеалгоритмовпри различныхусловиях. Выделяятолько частиалгоритма, которые вносятнаибольшийвклад во времяисполненияпрограммы, анализ помогаетопределить, доработка какихучастков кодапозволяетвнести максимальныйвклад в улучшениепроизводительности.
В программированиичасто приходитсяидти на различныекомпромиссы, которые могутсказыватьсяна производительности.Один алгоритмможет бытьбыстрее, но засчет использованиябольшого объемапамяти. Другойалгоритм, использующийколлекции, может бытьболее медленным, но зато егопроще разрабатыватьи поддерживать.
Послеанализа доступныхалгоритмов, понимания того, как они ведутсебя в различныхусловиях и ихтребованийк ресурсам, выможете выбратьоптимальныйалгоритм длявашей задачи.

==============16
Глава2. Списки
Существуетчетыре основныхспособа распределенияпамяти в VisualBasic: объявлениепеременныхстандартныхтипов (целые, с плавающейточкой и т.д.); объявлениепеременныхтипов, определенныхпользователем; создание экземпляровклассов припомощи оператораNewи изменениеразмера массивов.Существуетеще несколькоспособов, например, создание новогоэкземпляраформы или элементауправления, но эти способыне дают большихвозможностейпри созданиисложных структурданных.
Используяэти методы, можно легкостроить статическиеструктурыданных, такиекак большиемассивы определенныхпользователемтипов. Вы такжеможете изменятьразмер массивапри помощиоператораReDim.Тем не менее, перераспределениеданных можетбыть достаточносложным. Например, для того, чтобыпереместитьэлемент с одногоконца массивана другой, потребуетсяпереупорядочитьвесь массив, сдвинув всеэлементы наодну позицию, чтобы заполнитьосвободившеесяпространство.Затем можнопоместитьэлемент на егоновое место.
Динамическиеструктурыданных позволяютбыстро и легковыполнятьтакого родаизменения.Всего за несколькошагов можнопереместитьлюбой элементв структуреданных в любоедругое положение.
В этойглаве описаныметоды созданиядинамическихсписков в VisualBasic. Различныетипы списковобладают разнымисвойствами.Некоторые изних просты иобладают ограниченнойфункциональностью, другие же, такиекак циклическиесписки, одно или двусвязныесписки, являютсяболее сложнымии поддерживаютболее развитыесредства управленияданными.
В последующихглавах описанныеметоды используютсядля построениястеков, очередей, массивов, деревьев, хэш таблици сетей. Вамнеобходимоусвоить материалэтой главыперед тем, какпродолжитьчтение.Знакомствосо списками
Простейшаяформа списка —это группаобъектов. Онавключает в себяобъекты и позволяетпрограммеобращатьсяк ним. Если этовсе, что вамнужно от списка, вы можетеиспользоватьмассив в качествесписка, отслеживаяпри помощипеременнойNumInListчисло элементовв списке. Определивпри помощи этойпеременнойчисло имеющихсяэлементов, программа затемможет по очередиобратитьсяк ним в циклеForи выполнитьнеобходимыедействия.

=============17

Есливы в своей программеможете обойтисьэтим подходом, используйтеего. Этот методэффективен, и его легкоотлаживатьи поддерживатьблагодаря егопростоте. Темне менее, большинствопрограмм нестоль просты, и в них требуютсяболее сложныеконструкциидаже для такихпростых объектов, как списки.Поэтому в последующихразделах этойглавы обсуждаютсянекоторые путисоздания списковс большейфункциональностью.
В первомпараграфеописываютсяпути созданиясписков, которыемогут растии уменьшатьсясо временем.В некоторыхпрограммахнельзя заранееопределить, насколькобольшой списокпонадобится.Вы можете справитьсяс такой ситуациейпри помощисписка, которыйможет принеобходимостиизменять свойразмер.
В следующемпараграфеобсуждаютсянеупорядоченныесписки (unorderedlist), которыепозволяютудалять элементыиз любой частисписка. Неупорядоченныесписки даютбольший контрольнад содержимымсписка, чемпростые списки.Они также являютсяболее динамичными, так как позволяютизменять содержимоев произвольныймомент времени.
В последующихразделах обсуждаютсясвязные списки(linked list), которые используютуказателидля созданиячрезвычайногибких структурданных. Вы можетедобавлять илиудалять элементыиз любой частисвязного спискас минимальнымиусилиями. Вэтих параграфахтакже описанынекоторыеразновидностисвязных списков, такие какциклические, двухсвязныесписки илисписки со ссылками.Простыесписки
Еслив вашей программенеобходимсписок постоянногоразмера, выможете создатьего, простоиспользуямассив. В этомслучае можнопри необходимостиопрашиватьего элементыв цикле For.
Многиепрограммыиспользуютсписки, которыерастут илиуменьшаютсясо временем.Можно создатьмассив, соответствующиймаксимальновозможномуразмеру списка, но такое решениене всегда будетоптимальным.Не всегда можнозаранее знать, насколькобольшим можетстать список, кроме того, вероятность, что списокстанет оченьбольшим, можетбыть невелика, и созданныймассив гигантскихразмеров можетбольшую частьвремени лишьпонапраснузанимать память.Коллекции
Программаможет использоватьколлекцииVisual Basic дляхранения спискапеременногоразмера. МетодAddItemдобавляетэлемент в коллекцию.Метод Removeудаляет элемент.Следующийфрагмент кодадемонстрируетпрограмму, которая добавляеттри элементак коллекциии затем удаляетвторой элемент.

Dimlist As New Collection
Dimobj As MyClass
DimI As Integer

‘Создать и добавить1 элемент.
Setobj = New MyClass
list.Addobj

‘Добавить целоечисло.
i= 13
list.AddI

‘Добавить строку.
list.Add«Работа с коллекциями»

‘Удалить 2 элемент(целое число).
list.Remove2

===============18

Коллекциипытаются обеспечитьподдержку любыхприложений, и выполняютзамечательнуюработу. Их легкоиспользовать, они позволяютизвлекатьэлементы, проиндексированныепо ключу, и даютприемлемуюпроизводительность, если не содержатслишком многоэлементов.
Тем неменее, коллекциямсвойственныи определенныенедостатки.Для большихсписков, коллекциимогут работатьмедленнее, чеммассивы. Еслив вашей программене нужны всесвойства, предоставляемыеколлекцией, более быстрымможет бытьиспользованиепростого массива.
Схемахэширования, которую коллекциииспользуютдля управленияключами, такженакладываетряд ограничений.Во-первых, коллекциине позволяютдублироватьключи. Во-вторых, для коллекцииможно определить, какой элементимеет заданныйключ, но нельзяузнать, какойключ соответствуетданному элементу.И, наконец, коллекциине поддерживаютмножественныхключей. Например, может быть, вамхотелось бы, чтобы программамогла производитьпоиск по спискуслужащих, используяимя сотрудникаили его идентификационныйномер в системесоциальногострахования.Коллекция несможет поддерживатьоба методапоиска, так какона способнаоперироватьтолько однимключом.
В последующихпараграфахописываютсяметоды построениясписков, свободныхот этих ограничений.Списокпеременногоразмера
ОператорVisual BasicReDimпозволяетизменять размермассива. Выможете использоватьэто свойстводля построенияпростого спискапеременногоразмера. Начнитес объявлениябезразмерногомассива дляхранения элементовсписка. ТакжеопределитепеременнуюNumInListдля отслеживаниячисла элементовв списке. Придобавленииэлементов ксписку используйтеоператор ReDimдля увеличенияразмера массива, чтобы новыйэлемент могпоместитьсяв нем. При удаленииэлемента такжеиспользуйтеоператор ReDimдля уменьшениямассива иосвобожденияненужной большепамяти.

DimList() As String ‘ Списокэлементов.
DimNumInList As Integer ‘ Числоэлементовв списке.

SubAddToList(value As String)
‘Увеличитьразмер массива.
NumInList= NumInList + 1
ReDimPreserve List (1 To NumInList)

‘Добавить новыйэлемент к концусписка.
List(NumInList)= value
EndSub

SubRemoveFromList()
‘Уменьшитьразмер массива, освобождаяпамять.
NumInList= NumInList – 1
ReDimPreserve List (1 To NumInList)
EndSub

==================19

Этапростая схеманеплохо работаетдля небольшихсписков, но унее есть паранедостатков.Во-первых, приходитсячасто менятьразмер массива.Для созданиясписка из 1000элементов, придется 1000 разизменять размермассива. Хужетого, при увеличенииразмера списка, на изменениеего размерапотребуетсябольше времени, посколькупридется каждыйраз копироватьрастущий списокв памяти.
Дляуменьшениячастоты измененийразмера массива, можно добавлятьдополнительныеэлементы кмассиву приувеличенииего размера, например, по10 элементоввместо одного.При этом, когдавы будете добавлятьновые элементык списку в будущем, массив ужебудет содержатьнеиспользуемыеячейки, в которыевы сможетепоместить новыеэлементы безувеличенияразмера массива.Новое увеличениеразмера массивапотребуется, только когдапустые ячейкизакончатся.
Подобнымже образомможно избежатьизмененияразмера массивапри каждомудалении элементаиз списка. Можноподождать, покав массиве ненакопится 20неиспользуемыхячеек, преждечем уменьшатьего размер. Приэтом нужнооставить 10 свободныхячеек для того, чтобы можнобыло добавлятьновые элементыбез необходимостиснова увеличиватьразмер массива.
Заметим, что максимальноечисло неиспользуемыхячеек (20) должнобыть больше, чем минимальноечисло (10). Этоуменьшает числоизмененийразмера массивапри удаленииили добавленииего элементов.
Притакой схемев списке обычноесть несколькосвободныхячеек, тем неменее их числодостаточномало, и лишниезатраты памятиневелики. Свободныеячейки гарантируютвозможностьдобавленияили удаленияэлементов безизмененияразмера массива.Фактически, если вы неоднократнодобавляетек списку, а затемудаляете изнего один илидва элемента, вам может никогдане понадобитьсяизменять размермассива.

DimList() As String ‘ Списокэлементов.
DimArraySize As Integer ‘ Размермассива.
DimNumInList As Integer ‘ Числоиспользуемыхэлементов.

‘ Еслимассив заполнен, увеличить егоразмер, добавив10 ячеек.
‘ Затемдобавить новыйэлемент в конецсписка.
SubAddToList(value As String)
NumInList= NumInList + 1
IfNumInList > ArraySize Then
ArraySize= ArraySize + 10
ReDimPreserve List(1 To ArraySize)
EndIf
List(NumInList)= value
EndSub

‘ Удалитьпоследнийэлемент изсписка. Еслиосталось больше
‘ 20пустых ячеек, уменьшитьсписок, освобождаяпамять.
SubRemoveFromList()
NumInList= NumInList – 1
IfArraySize – NumInList > 20 Then
ArraySize= ArraySize –10
ReDimPreserve List(1 To ArraySize)
EndIf
EndSub

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

Дляочень большихмассивов эторешение можеттакже оказатьсяне самым лучшим.Если вам нуженсписок, содержащий1000 элементов, к которомуобычно добавляетсяпо 100 элементов, то все еще слишкоммного временибудет тратитьсяна изменениеразмера массива.Очевиднойстратегиейв этом случаебыло бы увеличениеприращенияразмера массивас 10 до 100 или болееячеек. Тогдаможно было быдобавлять по100 элементоводновременнобез частогоизмененияразмера списка.
Болеегибким решениембудет изменениеприращенияв зависимостиот размерамассива. Длянебольшихсписков этоприращениебыло бы такженебольшим. Хотяизмененияразмера массивапроисходилибы чаще, онипотребовалибы относительнонемного временидля небольшихмассивов. Длябольших списков, приращениеразмера будетбольше, поэтомуих размер будетизменятьсяреже.
Следующаяпрограммапытается поддерживатьпримерно 10 процентовсписка свободным.Когда массивзаполняется, его размерувеличиваетсяна 10 процентов.Если свободноепространствосоставляетболее 20 процентовот размерамассива, программауменьшает его.
Приувеличенииразмера массива, добавляетсяне меньше 10элементов, дажеесли 10 процентовот размерамассива составляютменьшую величину.Это уменьшаетчисло необходимыхизмененийразмера массива, если списокочень мал.
    продолжение
--PAGE_BREAK--
ПрограммаиспользуетпеременнуюLastCmdдля отслеживанияпоследнегоуправляющегообъекта в коллекции.Если вы выбираетекоманду Undo(Отменить) вменю Draw(Рисовать), топрограммауменьшаетзначение переменнойLastCmdна единицу.Когда программапотом выводитрисунок, онавызывает толькообъекты, стоящиедо объекта сномером LastCmd.
Есливы выбираетекоманду Redo(Повторить) вменю Draw, то программаувеличиваетзначение переменнойLastCmdна единицу.Когда программавыводит рисунок, она выводитна один объектбольше, чемраньше, поэтомуотображаетсявосстановленныйрисунок.
Придобавленииновой фигурыпрограммаудаляет любыекоманды изколлекции, которые лежатпосле позицииLastCmd,.затем добавляетновую командурисования вконце и запрещаеткоманду Redo, так как неткоманд, которыеможно было быотменить. Нарис. 13.1 показаноокно программыCommand2после добавленияновой фигуры.Контролирующийобъект
Контролирующийобъект (visitorobject) проверяетвсе элементыв составномобъекте (aggregateobject). Процедура, реализованнаяв составномклассе, обходитвсе объекты, передаваякаждый из нихконтролирующемуобъекту в качествепараметра.
Например, предположим, что составнойобъект хранитэлементы всвязном списке.Следующий кодпоказывает, как его методVisitобходит список, передаваякаждый объектв качествепараметраметоду Visitконтролирующегообъекта ListVisitor:

PublicSub Visit(obj As ListVisitor)
Dimcell As ListCell

Setcell = TopCell
DoWhile Not (cell Is Nothing)
obj.Visitcell
Setcell = cell.NextCell
Loop
EndSub

@Рис.13.1. ПрограммаCommand2

=========363

Следующийкод демонстрирует, как класс ListVisitorможет выводитьна экран значенияэлементов вокне Immediate(Срочно).

PublicSub Visit(cell As ListCell)
Debug.Printcell.Value
EndSub

Используяпарадигмуконтролирующегообъекта, составнойкласс определяетпорядок, в которомобходятсяэлементы. Составнойкласс можетопределятьнесколькометодов дляобхода содержащихего элементов.Например, классдерева можетобеспечиватьметоды VisitPreorder(Прямой обход),VisitPostorder(Обратный обход),VisitInorder(Симметричныйобход) и VisitBreadthFirst(Обход в глубину)для обходаэлементов вразличномпорядке.Итератор
Итераторобеспечиваетдругой методобхода элементовв составномобъекте. Объект итераторобращаетсяк составномуобъекту дляобхода егоэлементов, ив этом случаеитератор определяетпорядок, в которомпроверяютсяэлементы. Ссоставнымклассом могутбыть сопоставленынесколькоклассов итераторовдля того, чтобывыполнятьразличныеобходы элементовсоставногокласса.
Чтобывыполнить обходэлементов, итератор долженпредставлятьпорядок, в которомэлементы записаны, чтобы определитьпорядок ихобхода. Еслисоставной класспредставляетсобой связныйсписок, тообъект итератордолжен знать, что элементынаходятся всвязном списке, и должен уметьперемещатьсяпо списку. Таккак итераторуизвестны деталивнутреннегоустройствасписка, этонарушает скрытиеданных составногообъекта.
Вместотого чтобыкаждый класс, которому нужнопроверятьэлементы составногокласса, реализовалобход самостоятельно, можно сопоставитьсоставномуклассу класситератора.Класс итераторадолжен содержатьпростые процедурыMoveFirst(Переместитьсяв начало), MoveNext(Переместитьсяна следующийэлемент), EndOfList(Переместитьсяв конец списка)и CurrentItem(Текущий элемент)для обеспечениякосвенногодоступа к списку.Новые классымогут включатьв себя экземпляркласса итератораи использоватьего методы дляобхода элементовсоставногокласса. На рис.13.2 схематическипоказано, какновый объектиспользуетобъект итератордля связи сосписком.
ПрограммаIterTree, описанная ниже, используетитераторы дляобхода полногодвоичногодерева. КлассTraverser(Обходчик) содержитссылку наобъект итератор.Они используетобеспечиваемыеитераторомпроцедурыMoveFirst,MoveNext,CurrentCaptionи EndOfTreeдля получениясписка узловв дереве.

@Рис.13.2. Использованиеитератора длякосвенной связисо списком

=========364

Итераторынарушают скрытиесоответствующихим составныхобъектов, вотличие отновых классов, которые содержатитераторы. Длятого, чтобыизбавитьсяот потенциальнойпутаницы, можнорассматриватьитератор какнадстройкунад составнымобъектом.
Контролирующиеобъекты и итераторыобеспечиваютвыполнениепохожих функций, используяразличныеподходы. Таккак парадигмаконтролирующегообъекта оставляетдетали составногообъекта скрытымивнутри него, она обеспечиваетлучшую инкапсуляцию.Итераторы могутбыть полезны, если порядокобхода можетчасто изменятьсяили он долженпереопределятьсяво время выполненияпрограммы.Например, составнойобъект можетиспользоватьметоды порождающегокласса (которыйописан позднее)для созданияобъекта итераторав процессевыполненияпрограммы.Содержащийитератор классне должен знать, как создаетсяитератор, онвсего лишьиспользуетметоды итераторадля доступак элементамсоставногообъекта.Дружественныйкласс
Многиеклассы тесноработают сдругими. Например, класс итераторатесно взаимодействуетс составнымклассом. Длявыполненияработы, итератордолжен нарушатьскрытие составногокласса. Приэтом, хотя этисвязанныеклассы иногдадолжны нарушатьскрытие данныхдруг друга, другие классыдолжны не иметьтакой возможности.
Дружественныйкласс (friendclass) — этокласс, имеющийспециальноеразрешениенарушать скрытиеданных длядругого класса.Например, класситератораявляетсядружественнымклассом длясоответствующегосоставногокласса. Ему, вотличие отдругих классов, разрешенонарушать скрытиеданных длясоставногокласса.
В 5 йверсии VisualBasic появилосьзарезервированноеслово Friendдля разрешенияограниченногодоступа к переменными процедурам, определеннымвнутри модуля.Элементы, определенныепри помощизарезервированногослова Friend, доступны внутрипроекта, но нев других проектах.Например, предположим, что вы создаликлассы LinkedList(Связный список)и ListIterator(Итератор списка)в проекте ActiveXсервера. Программаможет создатьсервер связногосписка дляуправлениясвязными списками.Порождающийметод классаLinkedListможет создаватьобъекты типаListIteratorдля использованияв программе.
КлассLinkedListможет обеспечиватьв программесредства дляработы со связнымисписками. Этоткласс объявляетсвои свойстваи методы открытыми, чтобы их можнобыло использоватьв основнойпрограмме.Класс ListIteratorпозволяетпрограммевыполнятьитерации надобъектами, которыми управляеткласс LinkeList.Процедуры, используемыеклассом ListIteratorдля оперированияобъектамиLinkedList, объявляютсякак дружественныев модуле LinkedList.Если классыLinkedListи ListIteratorсоздаются водном и том жепроекте, токласс ListIteratorможет использоватьэти дружественныепроцедуры.Посколькуосновная программанаходится вдругом проекте, она этого сделатьне может.
Этоточень эффективный, но довольногромоздкийметод. Она требуетсоздания двухпроектов, иустановкиодного сервераActiveX.Он также неработает вболее раннихверсиях VisualBasic.
Наиболеепростой альтернативойбыло бы соглашениео том, что толькодружественныеклассы могутнарушать скрытиеданных другдруга. Если всеразработчикибудут придерживатьсяэтого правила, то проектомвсе еще можнобудет управлять.Тем не менее, искушениеобратитьсянапрямую кданным классаLinkedListможет бытьсильным, и всегдасуществуетвероятность, что кто нибудьнарушит скрытиеданных из залени или понеосторожности.
Другаявозможностьзаключаетсяв том, чтобыдружественныйобъект передавалсебя другомуклассу в качествепараметра.Передавая себяв качествепараметра, дружественныйкласс тем самымпоказывает, что он являетсятаковым. ПрограммаFstacksиспользуетэтот метод дляреализациистеков.

=======365

Прииспользованииэтого методавсе еще можнонарушить скрытиеданных объекта.Программа можетсоздать объектдружественногокласса и использоватьего в качествепараметра, чтобы обманутьпроцедурыдругого объекта.Тем не менее, это достаточногромоздкийпроцесс, ималовероятно, что разработчиксделает такслучайно.Интерфейс
В этойпарадигме одиниз объектоввыступает вкачестве интерфейса(interface) междудвумя другими.Один объектможет использоватьсвойства иметоды первогообъекта длявзаимодействиясо вторым. Интерфейсиногда такженазываетсяадаптером(adapter), упаковщиком(wrapper), или мостом(bridge). На рис.13.3 схематическиизображенаработа интерфейса.
Интерфейспозволяет двумобъектам наего концахизменятьсянезависимо.Например, еслисвойства объектаслева на рис.13.3 изменятся, интерфейсдолжен бытьизменен, а объектсправа — нет.
В этойпарадигмепроцедуры, используемыедвумя объектами, поддерживаютсяразработчиками, которые отвечаютза эти объекты.Разработчик, который реализуетлевый объект, также занимаетсяреализациейпроцедур интерфейса, которые взаимодействуютс левым объектом.Фасад
Фасад(Facade) аналогиченинтерфейсу, но он обеспечиваетпростой интерфейсдля сложногообъекта илигруппы объектов.Фасад такжеиногда называетсяупаковщиком(wrapper). На рис.13.4. показана схемаработы фасада.
Разницамежду фасадоми интерфейсомв основномумозрительная.Основная задачаинтерфейса —обеспечениекосвенноговзаимодействиямежду объектами, чтобы они моглиразвиватьсянезависимо.Основная задачафасада — облегчениеиспользованиякаких то сложныхвещей за счетскрытия деталей.Порождающийобъект
Порождающийобъект (Factory) —это объект, который создаетдругие объекты.Порождающийметод — этопроцедура илифункция, котораясоздает объект.
Порождающиеобъекты наиболееполезны, еслидва классадолжны тесноработать вместе.Например, составнойкласс можетсодержатьпорождающийметод, которыйсоздает итераторыдля него. Порождающийметод можетинициализироватьитератор такимобразом, чтобыон был готовк работе сэкземпляромкласса, которыйего создал.

@Рис.13.3 Интерфейс

========366

@Рис.13.4. Фасад

ПрограммаIterTreeсоздает полноедвоичное дерево, записанноев массиве. Посленажатия на однуиз кнопок, задающихнаправлениеобхода, программасоздает объектTraverser(Обходчик). Онатакже используетодин из порождающихметодов деревадля созданиясоответствующегоитератора.Объект Traverserиспользуетитератор дляобхода дереваи вывода спискаузлов в правильномпорядке. Нарис. 13.5 приведеноокно программыIterTree, показывающееобратный обходдерева.Единственныйобъект
Единственныйобъект (singletonobject) — этообъект, которыйсуществуетв приложениив единственномэкземпляре.Например, вVisual Basicопределен классPrinter(Принтер). Онтакже определяетединственныйобъект с темже названием.Этот объектпредставляетпринтер, выбранныйв системе поумолчанию. Таккак в каждыймомент времениможет бытьвыбран толькоодин принтер, то имеет смыслопределитьобъект Printerкак единственныйобъект.
Одиниз способовсозданияединственногообъекта заключаетсяв использованиипроцедуры, работающейсо свойствамив модуле BAS.Эта процедуравозвращаетссылку на объект, определенныйвнутри модулякак закрытый.Для другихчастей программыэта процедуравыглядит какпросто еще одинобъект.

@Рис.13.5. ПрограммаIterTree, демонстрирующаяобратный обход

=======367
ПрограммаWinListиспользуетэтот подходдля созданияединственногообъекта классаWinListerClass.Объект классаWinListerClassпредставляетокна в системе.Так как операционнаясистема одна, то нужен толькоодин объекткласса WinListerClass.Модуль WinList.BASиспользуетследующий коддля созданияединственногообъекта с названиемWindowLister.

Privatem_WindowLister As New WindowListerClass

PropertyGet WindowLister() As WindowListerClass
SetWindowLister = m_WindowLister
EndProperty

Единственныйобъект WindowListerдоступен вовсем проекте.Следующий коддемонстрирует, как основнаяпрограммаиспользуетсвойство WindowListэтого объектадля вывода наэкран спискаокон.

WindowListText.Text= WindowLister.WindowList
Преобразованиев последовательнуюформу
Многиеприложениясохраняютобъекты ивосстанавливаютих позднее.Например, приложениеможет сохранятькопию своихобъектов втекстовомфайле. При следующемзапуске программы, она считываетэто файл и загружаетобъекты.
Объектможет содержатьпроцедуры, которые считываюти записываютего в файл. Общийподход можетзаключатьсяв том, чтобысоздать процедуры, которые сохраняюти восстанавливаютданные объекта, используястроку. Посколькузапись данныхобъекта в однойстроке преобразуетобъект в последовательностьсимволов, этотпроцесс иногданазываетсяпреобразованиемв последовательнуюформу (serialization).
Преобразованиеобъекта в строкуобеспечиваетбольшую гибкостьосновной программы.При этом онаможет сохранятьи считыватьобъекты, используятекстовыефайлы, базуданных илиобласть памяти.Она может переслатьпредставленныйтаким образомобъект по сетиили сделатьего доступнымна Web странице.Программа илиэлемент ActiveXна другом концеможет использоватьпреобразованиеобъекта в строкудля воссозданияобъекта. Программатакже можетдополнительнообработатьстроку, например, зашифроватьее после преобразованияобъекта в строкуи расшифроватьперед обратнымпреобразованием.
Одиниз подходовк преобразованиюобъекта впоследовательнуюформу заключаетсяв том, чтобыобъект записалвсе свои данныев строку заданногоформата. Например, предположим, что класс Rectangle(Прямоугольник)имеет свойстваX1,Y1,X2и Y2.Следующий коддемонстрирует, как класс можетопределятьпроцедурысвойстваSerialization:

PropertyGet Serialization() As String
Serialization= _
Format$(X1)& ";" & Format$(Y1) & ";" & _
Format$(X2)& ";" & Format$(Y2) & ";"
EndProperty

PropertyLet Serialization(txt As String)
Dimpos1 As Integer
Dimpos2 As Integer

pos1= InStr(txt, ";")
X1= CSng(Left$(txt, pos1 — 1))
pos2= InStr(pos1 + 1, txt, ";")
Y1= CSng(Mid$(txt, pos1 + 1, pos2 – pos1 — 1))
pos1= InStr(pos2 + 1, txt, ";")
X2= CSng(Mid$(txt, pos2 + 1, pos1 — pos2 — 1))
pos2= InStr(pos1 + 1, txt, ";")
Y2= CSng(Mid$(txt, pos1 + 1, pos2 – pos1 — 1))
EndProperty

Этотметод довольнопростой, но неочень гибкий.По мере развитияпрограммы, изменения вструктуреобъектов могутзаставить васперетранслироватьвсе сохраненныеранее преобразованныев последовательнуюформу объекты.Если они находятсяв файлах илибазах данных, для загрузкистарых данныхи записи их вновом форматеможет потребоватьсянаписаниепрограмм конверторов.
Болеегибкий подходзаключаетсяв том, чтобысохранятьвместе со значениямиэлементовданных объектаих имена. Когдаобъект считываетданные, преобразованныев последовательнуюформу, он используетимена элементовдля определениязначений, которыйнеобходимоустановить.Если позднеев определениеэлемента будутдобавленыкакие либоэлементы, илиудалены изнего, то не придетсяпреобразовыватьстарые данные.Если новыйобъект загрузитстарые данные, то он простопроигнорируетне поддерживаемыеболее значения.
Определяязначения данныхпо умолчанию, иногда можноуменьшитьразмер преобразованныхв последовательнуюформу объектов.Процедура getсвойстваSerializationсохраняеттолько значения, которые отличаютсяот значенийпо умолчанию.Перед тем, какпроцедура letсвойства начнетвыполнениепреобразованияв последовательнуюформу, онаинициализируетвсе элементыобъекта значениямипо умолчанию.Значения, неравные значениямпо умолчанию, обновляютсяпо мере обработкиданных процедурой.
ПрограммаShapesиспользуетэтот подходдля сохраненияи загрузки сдиска рисунков, содержащихэллипсы, линии, и прямоугольники.Объект ShapePictureпредставляетвесь рисунокцеликом. Онсодержит коллекциюуправляющихобъектов, которыепредставляютразличныефигуры.
Следующийкод демонстрируетпроцедурысвойстваSerializationобъекта ShapePicture.Объект ShapePictureсохраняет имятипа для каждогоиз типов объектов, а затем в скобках —представлениеобъекта впоследовательнойформе.

PropertyGet Serialization() As String
Dimtxt As String
Dimi As Integer

Fori = 1 To LastCmd
txt= txt & _
TypeName(CmdObjects(i))& "(" & _
CmdObjects(i).Serialization& ")"
NextI
Serialization= txt
EndProperty

==========369

ПроцедураletсвойстваSerializationиспользуетподпрограммуGetSerializationдля чтенияимени объектаи списка данныхв скобках. Например, если объектShapePictureсодержит командурисованияпрямоугольника, то его представлениев последовательнойформе будетвключать строку“RectangleCMD”, за которойбудут следоватьданные, представленныев последовательнойформе.
ПроцедураиспользуетподпрограммуCommandFactoryдля созданияобъекта соответствующеготипа, а затемзаставляетновый объектпреобразоватьсебя из последовательнойформы представления.
    продолжение
--PAGE_BREAK--
PropertyLet Serialization(txt As String) Dim pos As Integer Dim token_name AsString Dim token_value As String Dim and As Object
'Start a new picture.
NewPicture
'Read values until there are no more.
GetSerializationtxt, pos, token_name, token_value Do While token_name ""
'Make the object and make it unserialize itself.
Setand = ConiniandFactory(token_name)
IfNot (and Is Nothing) Then _
and.Serialization= token_value
GetSerializationtxt, pos, token_name, tokerL-value Loop
LastCmd= CmdObjects.Count End Property
ПарадигмаМодель/Вид/Контроллер.
ПарадигмаМодель/Вид/Контроллер(МВК) (Model/View/Controller)позволяетпрограммеуправлятьсложнымисоотношениямимежду объектами, которые сохраняютданные, объектами, которые отображаютих на экране, и объектами, которые оперируютданными. Например, приложениеработы с финансамиможет выводитьданные о расходахв виде таблицы, секторнойдиаграммы, илиграфика. Еслипользовательизменяет значениев таблице, приложениедолжно автоматическиобновить изображениена экране. Можеттакже понадобитьсязаписать измененныеданные на диск.
Длясложных системуправлениевзаимодействиеммежду объектами, которые хранят, отображаюти оперируютданными, можетбыть достаточнозапутанным.ПарадигмаМодель/Вид/Контроллерразбиваетвзаимодействия, так что можноработать с нимипо отдельности, при этом используютсятри типа объектов: модели, виды, и контроллеры.Модели
Модель(Model) представляетданные, обеспечиваяметоды, которыедругие объектымогут использоватьдля проверкии измененияданных. В приложенииработы с финансовымиданными, модельсодержит данныео расходах. Онаобеспечиваетпроцедуры дляпросмотра иизменениязначений расходови ввода новыхзначений. Онатакже можетобеспечиватьфункции длявычислениясуммарныхвеличин, такихкак полныеиздержки, расходыпо подразделениям, средние расходыза месяц, и такдалее
Модельвключает в себянабор видов, которые отображаютданные. Приизмененииданных, модельсообщает обэтом видам, которые изменяютизображениена экранесоответствующимобразом.Виды
Вид(View) отображаетпредставленныев модели данные.Так как видыобычно выводятданные дляпросмотрапользователем, иногда удобнеесоздавать их, используяформу, а не класс.
Когдапрограммасоздает вид, она должнадобавить егок набору видовмодели.Контроллеры
Контроллер(Controller) изменяетданные в модели.Контроллердолжен всегдаобращатьсяк данным моделичерез ее открытыеметоды. Этиметоды могутзатем сообщатьоб изменениивидам. Есликонтроллеризменял быданные моделинепосредственно, то модель несмогла бы сообщитьоб этом видам.Виды/Контроллеры
Многиеобъекты одновременноотображаюти изменяютданные. Например, текстовое полепозволяетпользователювводить ипросматриватьданные. Форма, содержащаятекстовое поле, является одновременнои видом, и контроллером.Переключатели, поля выбораопций, полосыпрокрутки, имногие другиеэлементыпользовательскогоинтерфейсапозволяютодновременнопросматриватьи оперироватьданными.
Видами/контроллерамипроще всегоуправлять, еслипопытатьсямаксимальноразделитьфункции просмотраи управления.Когда объектизменяет данные, он не долженсам обновлятьизображениена экране. Онможет сделатьэто позднее, когда модельсообщит емукак виду опроизошедшемизменении.
Этиметоды достаточногромоздки дляреализациистандартныхобъектовпользовательскогоинтерфейса, таких как текстовыеполя. Когдапользовательвводит значениев текстовомполе, оно немедленнообновляется, и выполнятсяего обработчиксобытия Change.Этот обработчиксобытий можетмодель об изменении.Модель затемсообщаетвиду/контроллеру(выступающемутеперь как вид)о произошедшемизменении. Еслипри этом объектобновит текстовоеполе, то произойдетеще одно событиеChange, о котором сновабудет сообщеномодели и программавойдет в бесконечныйцикл.
Чтобыпредотвратитьэту проблему, методы, изменяющиеданные в модели, должны иметьнеобязательныйпараметр, указывающийна контроллер, который вызвалэти изменения.Если виду/контроллерутребуетсясообщить обизменении, которое онвызывает, ондолжен передатьзначение Nothingпроцедуре, вносящей изменения.Если этого нетребуется, тов качествепараметраобъект долженпередаватьсебя.

=========371

@Рис.13.6. ПрограммаExpMVC

ПрограммаExpMVC, показаннаяна рис. 13.6, используетпарадигмуМодель/Вид/Контроллердля выводаданных о расходах.На рисункепоказаны тривида различныхтипов. Вид/контроллерTableViewотображаетданные в таблице, при этом можноизменять названиястатей расходови их значенияв соответствующихполях.
Вид/контроллерGraphViewотображаетданные припомощи гистограммы, при этом можноизменять значениярасходов, двигаястолбики припомощи мышивправо.
ВидPieViewотображаетсекторнуюдиаграмму. Этопросто вид, поэтому егонельзя использоватьдля измененияданных.Резюме
Классыпозволяютпрограммистамна Visual Basicрассматриватьстарые задачис новой точкизрения. Вместотого чтобыпредставлятьсебе длиннуюпоследовательностьзаданий, котораяприводит квыполнениюзадачи, можнодумать о группеобъектов, которыеработают, совместновыполняя задачу.Если задачаправильноразбита начасти, то каждыйиз классов поотдельностиможет бытьочень простым, хотя все вместеони могут выполнятьочень сложнуюфункцию. Используяописанные вэтой главепарадигмы, выможете разбитьклассы так, чтобы каждыйиз них оказалсямаксимальнопростым.

==============372Требованияк аппаратномуобеспечению
Длязапуска и измененияпримеров приложенийвам понадобитсякомпьютер, который удовлетворяеттребованиямVisual Basic каппаратномуобеспечению.
Алгоритмвыполняютсяс различнойскоростью накомпьютерахразных конфигураций.Компьютер спроцессоромPentium Pro и64 Мбайт памятибудет быстреекомпьютерас 386 процессороми 4 Мбайт памяти.Вы быстро узнаетеограничениявашего оборудования.Выполнениепрограмм примеров
Одиниз наиболееполезных способоввыполненияпрограмм примеров —запускать ихпри помощивстроенныхсредств отладкиVisual Basic.Используя точкиостанова, просмотрзначений переменныхи другие свойстваотладчика, выможете наблюдатьалгоритмы вдействии. Этоможет бытьособенно полезнодля пониманиянаиболее сложныхалгоритмов, таких как алгоритмыработы сосбалансированнымидеревьями исетевые алгоритмы, представленныев 7 и 12 главахсоответственно.
Некоторыеи программпримеров создаютфайлы данныхили временныефайлы. Эти программыпомещают такиефайлы в соответствующиедиректории.Например, некоторыеиз программсортировки, представленныев 9 главе, создаютфайлы данныхв директорииSrc\Ch9/.Все эти файлыимеют расширение“.DAT”, поэтому выможете найтии удалить ихв случае необходимости.
Программыпримеровпредназначенытолько длядемонстрационныхцелей, чтобыпомочь вампонять определенныеконцепцииалгоритмов, и в них не почтине реализованаобработкаошибок илипроверка данных.Если вы введетенеправильноерешение, программаможет аварийнозавершитьработу. Есливы не знаете, какие данныедопустимы, воспользуйтесьдля полученияинструкцийменю Help(Помощь) программы.

========374

A
addressing
indirect 42
open 278
adjacency matrix 75
aggregate object 337
ancestor 122
array
triangular 75
augmenting path 320
B
B+Tree 11
balanced profit 196
base case 88
best case 23
binary hunt and search 260
binary search 254
branch 122
branchandbound technique 180
bubblesort 224
bucketsort 243
C
cells 40
child 122
circular referencing problem 50
collision resolution policy 265
command 336
complexity theory 14
controller 345
countingsort 242
critical path 317
cycle 293
D
data abstraction 329
decision tree 180
delegation 334
descendant 122
E
edge 293
encapsulation 328
exhaustive search 180, 250
expected case 23
F
facade 341
factorial 87
factory 341
fake pointer 27, 56
fat node 11, 123
Fibonacci numbers 92
firehouse problem 211
FirstInFirstOut 63
forward star 11, 79, 126
friend class 339
functors 336
G
game tree 180
garbage collection 37
garbage value 37
generic 331
graph 122, 293
greatest common divisor 90
greedy algorithms 300
H
Hamiltonian path 210
hashing 264
heap 235
heapsort 235
heuristic 180
Hilbert curves 94
hillclimbing 193
I
implements 332
incremental improvements 199
inheritance 334
insertionsort 222
interface 340
interpolation search 255
interpolative hunt and search 262
K
knapsack problem 188
L
label correcting 303
label setting 303
LastInFirstOut list 60
leastcost 195
linear probing 278
link 293
list
circular 49
doubly linked 50
linked 31
threaded 53
unordered 31, 36
M
mergesort 233
minimal spanning tree 299
minimax 182
model 345
Model/View/Controller 345
Monte Carlo search 197
N
network 293
capacitated 319
capacity 319
connected 293
directed 293
flow 319
residual 320
node 122, 293
node
degree 123
internal 123
sibling 122
O
octtree 152
optimum
global 203
local 203
P
page file 26
parent 122
partition problem 209
path 293
pointers 27
pointtopoint shortest path 312
polymorphism 328, 331
primary clustering 280
priority queue 238
probe sequence 265
pruning 187
pseudorandom probing). 287
Q
quadratic probing 285
quadtree 122, 145
queue 63
circular 65
multi-headed 72
priority 70
quicksort 228
R
random search 197
recursion
direct 86
indirect 87
multiple 21
tail recursion 105
recursive procedure 20
redundancy 325
reference counter 28
rehashing 290
relatively prime 90
residual capacity 320
reuse 328, 334
S
satisfiability problem 208
secondary clustering 286
selectionsort 219
sentinel 45
serialization 342
shortest path 302
Sierpinski curves 98
simulated annealing 204
singleton object 341
sink 319
source 319
spanning tree 298
stack 60
subtree 122
T
tail recursion removal 106
thrashing 26
thread 53
traveling salesman problem 211
traversal
breadth-first 131
depth-first 131
inorder 130
postorder 130
divorder 130
tree 122
AVL tree 154
B-tree 166
B+tree 170
binary 123
bottom-up B-trees 170
complete 129
depth 123
left rotation 156
left-right rotation 157
right rotation 156
right-left rotation 157
symmetrically threaded 141
ternary 123
threaded 122
top-down B-tree 170
traversing 130
tries 122
turn penalties 314
U
unsorting 221
V
view 345
virtual memory 26
visitor object 337
W
work assignment 327
worst case 23

Дружественный класс 339
А
Абстракция данных 329
Адресация
косвенная 42
открытая 278
Алгоритм
поглощающий 300
Г
Гамильтонов путь 210
Граф 122, 293
Д
Делегирование 334
Деревья 122
АВЛ-деревья 154
Б-деревья 166
Б+деревья 11, 170, 171
ветвь 122
внутренний узел 123
восьмеричные 152
вращения 155
двоичные 123
дочерний узел 122
игры 180
квадродеревья 145
корень 122
лист 122
нисходящие Б-деревья 170
обратный обход 130
обход 130
обход в глубину 131
обход в ширину 131
поддерево 122
полные 129
порядок 123
потомок 122
предок 122
представление нумерацией связей 11, 126
прямой обход 130
решений 180
родитель 122
с полными узлами 11
с симметричными ссылками 141
симметричный обход 130
троичные 123
узел 122
упорядоченные 135
З
Задача
коммивояжера 211
о выполнимости 208
о пожарных депо 211
о разбиении 209
поиска Гамильтонова пути 210
распределения работы 327
формирования портфеля 188
Значение
\ 37
И
Инкапсуляция 328
К
Ключи
объединение 216
сжатие 216
Коллекция 31
Кратчайший маршрут
двухточечный 312
дерево кратчайшего маршрута 302
для всех пар 312, 313
коррекция меток 303, 308
со штрафами за повороты 312, 314
установка меток 303, 304
Кривые
Гильберта 94
Серпинского 98
М
Массив
нерегулярный 78
представление в виде прямой звезды 79
разреженный 80
треугольный 75
Матрица смежности 75
Метод
ветвей и границ 180, 187
восхождения на холм 193
минимаксный 182
Монте-Карло 197
наименьшей стоимости 195
отжига 204
полного перебора 180
последовательных приближений 199
сбалансированной прибыли 196
случайного поиска 197
эвристический 180
Модель/Вид/Контроллер 345
Н
Наибольший общий делитель 90
Наследование 334
О
Объект
вид 345
единственный 341
интерфейс 340
итератор 338
контролирующий 337
контроллер 345
модель 345
порождающий 341
преобразование в последовательную форму 342
составной 337
управляющий 336
фасад 341
Ограничение 334
Оптимум
глобальный 203
локальный 203
Очередь 63
многопоточная 72
приоритетная 70, 238
циклическая 65
П
Память
виртуальная 26
пробуксовка 26
чистка 37
Пирамида 235
Повторное использование 334
Поиск
двоичный 254
интерполяционный 255
методом полного перебора 250
следящий 260
Полиморфизм 331
Потоки 53
Проблема циклических ссылок 50
Процедура
очистки памяти 38
рекурсивная 20
Псевдоуказатели 27, 56
Р
Разрешение конфликтов 265
Рекурсия
восходящая 154
косвенная 21, 87
многократная 21
прямая 86
условие остановки 88
хвостовая 105
С
Сеть 293
избыточность 325
источник 319
кратчайший маршрут 302
критический путь 317
нагруженная 319
наименьшее остовное дерево 299
ориентированная 293
остаточная 320
остаточная пропускная способность 320
остовное дерево 297
поток 319
пропускная способность 319
простой путь 293
путь 293
расширяющий путь 320
ребро 293
связная 293
связь 293
сток 319
узел 293
цена связи 293
цикл 293
Сигнальная метка 45
Системный стек 22
Случай
наилучший 23
наихудший 23
ожидаемый 23
Сортировка
блочная 243
быстрая 228
вставкой 222
выбором 219
пирамидальная 235
подсчетом 242
пузырьковая 224
рандомизация 221
слиянием 233
Список
двусвязный 50
многопоточный 53
неупорядоченный 31, 36
первый вошел-первый вышел 63
первый вошел-последний вышел 60
связный 31
циклический 49
Стек 60
Странный аттрактор 150
Счетчик ссылок 28
Т
Теория
сложности алгоритмов 14
хаоса 151
Тестовая последовательность
вторичная кластеризация 286
квадратичная проверка 284
линейная проверка 278
первичная кластеризация 280
псевдослучайная проверка 287
У
Указатели 27, 31
Ф
Файл подкачки 26
Факториал 87
Х
Хеширование 264
блоки 269
открытая адресация 278
разрешение конфликтов 265
рехеширование 290
связывание 266
тестовая последовательность 265
хеш-таблица 264
Ч
Числа
взаимно простые 90
Фибоначчи 92
Я
Ячейка 40    продолжение
--PAGE_BREAK--
ConstWANT_FREE_PERCENT = .1 ‘ 10% свободногоместа.
ConstMIN_FREE = 10 ‘ Минимальноечисло пустыхячеек.
GlobalList() As String ‘ Массивэлементовсписка.
GlobalArraySize As Integer ‘ Размермассива.
GlobalNumItems As Integer ‘ Числоэлементовв списке.
GlobalShrinkWhen As Integer ‘ Уменьшитьразмер, если NumItems

‘ Еслимассив заполнен, увеличить егоразмер.
‘ Затемдобавить новыйэлемент в конецсписка.
SubAdd(value As String)
NumItems= NumItems + 1
IfNumItems > ArraySize Then ResizeList
List(NumItems)= value
EndSub

‘ Удалитьпоследнийэлемент изсписка.
‘ Еслив массиве многопустых ячеек, уменьшить егоразмер.
SubRemoveLast()
NumItems= NumItems – 1
IfNumItems
EndSub

‘ Увеличитьразмер массива, чтобы 10% ячеекбыли свободны.
SubResizeList()
Dimwant_free As Integer
want_free= WANT_FREE_PERCENT * NumItems
Ifwant_free
ArraySize= NumItems + want_free
ReDimPreserve List(1 To ArraySize)

‘Уменьшитьразмер массива, если NumItems
ShrinkWhen= NumItems – want_free
EndSub

===============21
КлассSimpleList
Чтобыиспользоватьэтот простойподход, программенеобходимознать все параметрысписка, приэтом нужноследить заразмером массива, числом используемыхэлементов, ит.д. Если необходимосоздать большеодного списка, потребуетсямножество копийпеременныхи код, управляющийразными списками, будет дублироваться.
КлассыVisual Basicмогут сильнооблегчитьвыполнениеэтой задачи.Класс SimpleListинкапсулируетэту структурусписка, упрощаяуправлениесписками. Вэтом классеприсутствуютметоды Addи Removeдля использованияв основнойпрограмме. Внем также естьпроцедурыизвлечениясвойств NumItemsи ArraySize, с помощью которыхпрограмма можетопределитьчисло элементовв списке и объемзанимаемойим памяти.
ПроцедураResizeListобъявлена какчастная внутрикласса SimpleList.Это скрываетизменениеразмера спискаот основнойпрограммы, поскольку этоткод должениспользоватьсятолько внутрикласса.
Используякласс SimpleList, легко создатьв приложениинесколькосписков. Длятого чтобысоздать новыйобъект длякаждого списка, просто используетсяоператор New.Каждый из объектовимеет своипеременные, поэтому каждыйиз них можетуправлятьотдельнымсписком:

DimList1 As New SimpleList
DimList2 As New SimpleList

Когдаобъект SimpleListувеличиваетмассив, он выводитокно сообщения, показывающееразмер массива, количествонеиспользуемыхэлементов внем, и значениепеременнойShrinkWhen.Когда числоиспользованныхячеек в массивестановитсяменьше, чемзначение ShrinkWhen, программауменьшаетразмер массива.Заметим, чтокогда массивпрактическипуст, переменнаяShrinkWhenиногда становитсяравной нулюили отрицательной.В этом случаеразмер массиване будет уменьшаться, даже если выудалите всеэлементы изсписка.

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

ПрограммаSimListдобавляет кмассиву еще50 процентовпустых ячеек, если необходимоувеличить егоразмер, и всегдаоставляет приэтом не менее1 пустой ячейки.Эти значениябыл выбраныдля удобстваработы с программой.В реальномприложении, процент свободнойпамяти долженбыть меньше, а число свободныхячеек больше.Более разумнымв таком случаебыло бы выбратьзначения порядка10 процентов оттекущего размерасписка и минимум10 свободныхячеек.Неупорядоченныесписки
В некоторыхприложенияхможет понадобитьсяудалять элементыиз серединысписка, добавляяпри этом элементыв конец списка.В этом случаепорядок расположенияэлементов можетбыть не важен, но при этомможет бытьнеобходимоудалять определенныеэлементы изсписка. Спискитакого типаназываютсянеупорядоченнымисписками (unorderedlists). Они такжеиногда называются«множествомэлементов».
Неупорядоченныйсписок долженподдерживатьследующиеоперации:
добавление элемента к списку;
удаление элемента из списка;
определение наличия элемента в списке;
выполнение каких либо операций (например, вывода на дисплей или принтер) для всех элементов списка.
Простуюструктуру, представленнуюв предыдущемпараграфе, можно легкоизменить длятого, чтобыобрабатыватьтакие списки.Когда удаляетсяэлемент изсередины списка, остальныеэлементы сдвигаютсяна одну позицию, заполняяобразовавшийсяпромежуток.Это показанона рис. 2.1, на которомвторой элементудаляется изсписка, и третий, четвертый, ипятый элементысдвигаютсявлево, заполняясвободныйучасток.
Удалениеиз массиваэлемента притаком подходеможет занятьдостаточномного времени, особенно еслиудаляетсяэлемент в началесписка. Чтобыудалить первыйэлемент измассива с 1000элементов, потребуетсясдвинуть влевона одну позицию999 элементов.Гораздо быстрееудалять элементыможно при помощипростой схемычистки памяти(garbage collection).
Вместоудаления элементовиз списка, пометьтеих как неиспользуемые.Если элементысписка — данныепростых типов, например целые, можно помечатьэлементы, используяопределенное, так называемое«мусорное»значение (garbagevalue).

@Рисунок2.1 Удаление элементаиз серединымассива

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

Дляцелых чиселможно использоватьдля этого значение 32.767. Для переменнойтипа Variantможно использоватьзначение NULL.Это значениеприсваиваетсякаждому неиспользуемомуэлементу. Следующийфрагмент кодадемонстрируетудаление элементаиз подобногоцелочисленногосписка:

ConstGARBAGE_VALUE = -32767

‘ Пометитьэлемент какнеиспользуемый.
SubRemoveFromList(position As Long)
List(position)= GARBAGE_VALUE
EndSub

Еслиэлементы списка —это структуры, определенныеоператоромType, вы можете добавитьк такой структуреновое полеIsGarbage.Когда элементудаляется изсписка, значениеполя IsGarbageустанавливаетсяв True.

TypeMyData
NameAs Sring ‘ Данные.
IsGarbageAs Integer ‘ Этот элементне используется?
EndType

‘ Пометитьэлемент, какне использующийся.
SubRemoveFromList (position As Long)
List(position).IsGarbage= True
EndSub

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

‘ Печатьэлементовсписка.
SubPrintItems()
DimI As Long

ForI = 1 To ArraySize
IfNot IsNull(List(I)) Then ‘Если элементне помечен
PrintStr$(List(I)) ‘ напечататьего.
EndIf
NextI
EndSub

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

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

Длятого, чтобыизбежать этого, можно периодическизапускатьпроцедуруочистки памяти(garbage collectionroutine). Эта процедураперемещаетвсе непомеченныезаписи в началомассива. Послеэтого можнодобавить ихк свободнымэлементам вконце массива.Когда потребуетсядобавить кмассиву дополнительныеэлементы, ихтакже можнобудет использоватьбез измененияразмера массива.
Последобавленияпомеченныхэлементов кдругим свободнымячейкам массива, полный объемсвободногопространстваможет статьдостаточнобольшим, и вэтом случаеможно уменьшитьразмер массива, освобождаяпамять:

PrivateSub CollectGarbage()
Dimi As Long
Dimgood As Long

good= 1 ‘ Первый используемыйэлемент.
Fori = 1 To m_NumItems
‘Если он не помечен, переместитьего на новоеместо.
IfNot IsNull(m_List(i)) Then
m_List(good)= m_list(i)
good= good + 1
EndIf
Nexti

‘Последнийиспользуемыйэлемент.
m_NumItems(good)= good — 1
‘Необходимоли уменьшатьразмер списка?
Ifm_NumItems
EndSub

Привыполнениичистки памяти, используемыеэлементы перемещаютсяближе к началусписка, заполняяпространство, которое занималипомеченныеэлементы. Значит, положениеэлементов всписке можетизменитьсяво время этойоперации. Еслидругие частьпрограммыобращаютсяк элементамсписка по ихположению внем, необходимомодифицироватьпроцедуручистки памяти, с тем, чтобыона также обновлялассылки на положениеэлементов всписке. В общемслучае этоможет оказатьсядостаточносложным, приводяк проблемампри сопровождениипрограмм.
Можновыбирать разныемоменты длязапуска процедурычистки памяти.Один из них —когда массивдостигаетопределенногоразмера, например, когда списоксодержит 30000элементов.
Этомуметоду присущиопределенныенедостатки.Во первых, ониспользуетбольшой объемпамяти. Есливы часто добавляетеили удаляетеэлементы, «мусор»будет заниматьдовольно большуючасть массива.При таком неэкономномрасходованиипамяти, программаможет тратитьвремя на свопинг, хотя списокмог бы целикомпомещатьсяв памяти приболее частомпереупорядочивании.

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

Во-вторых, если списокначинает заполнятьсяненужнымиданными, процедуры, которые егоиспользуют, могут статьчрезвычайнонеэффективными.Если в массивеиз 30.000 элементов25.000 не используются, подпрограмматипа описаннойвыше PrintItems, может выполнятьсяужасно медленно.
И, наконец, чистка памятидля очень большогомассива можетпотребоватьзначительноговремени, вособенности, если при обходеэлементовмассива программеприходитсяобращатьсяк страницам, выгруженнымна диск. Этоможет приводитьк «подвисанию»вашей программына несколькосекунд во времячистки памяти.
Чтобырешить этупроблему, можносоздать новуюпеременнуюGarbageCount, в которой будетнаходитьсячисло ненужныхэлементов всписке. Когдазначительнаячасть памяти, занимаемойсписком, содержитненужные элементы, вы может начатьпроцедуру«сборки мусора».

DimGarbageCount As Long ‘ Числоненужных элементов.
DimMaxGarbage As Long ‘ Это значениеопределяетсяв ResizeList.

‘ Пометитьэлемент какненужный.
‘ Если«мусора» слишкоммного, начатьчистку памяти.
PublicSub Remove(position As Long)
m_List(position)= Null
m_GarbageCount= m_GarbageCount + 1

‘Если «мусора»слишком много, начать чисткупамяти.
Ifm_GarbageCount > m_MaxGarbage Then CollectGarbage
EndSub

ПрограммаGarbageдемонстрируетэтот методчистки памяти.Она пишет рядомс неиспользуемымиэлементамисписка слово«unused», а рядомс помеченнымикак ненужные —слово «garbage».Программаиспользуеткласс GarbageListпримерно также, как программаSimListиспользовалакласс SimpleList, но при этом онаеще осуществляет«сборку мусора».
Чтобыдобавить элементк списку, введитеего значениеи нажмите накнопку Add(Добавить). Дляудаления элементавыделите его, а затем нажмитена кнопку Remove(Удалить). Еслисписок содержитслишком много«мусора», программаначнет выполнятьчистку памяти.
Прикаждом измененииразмера спискаобъекта GarbageList, программавыводит окносообщения, вкотором приводитсячисло используемыхи свободныхэлементов всписке, а такжезначения переменныхMaxGarbageи ShrinkWhen.Если удалитьдостаточноеколичествоэлементов, такчто больше, чемMaxGarbageэлементов будутпомечены какненужные, программаначнет выполнятьчистку памяти.После ее окончания, программауменьшаетразмер массива, если он содержитменьше, чемShrinkWhenзанятых элементов.
Еслиразмер массивадолжен бытьувеличен, программаGarbageдобавляет кмассиву еще50 процентовпустых ячеек, и всегда оставляетхотя бы однупустую ячейкупри любом измененииразмера массива.Эти значениябыли выбраныдля упрощенияработы пользователясо списком. Вреальной программепроцент свободнойпамяти долженбыть меньше, а число свободныхячеек — больше.Оптимальнымивыглядят значенияпорядка 10 процентови 10 свободныхячеек.

==========26
Связныесписки
Другаястратегияиспользуетсяпри управлениисвязаннымисписками. Связанныйсписок хранитэлементы вструктурахданных илиобъектах, которыеназываютсяячейками (cells).Каждая ячейкасодержит указательна следующуюячейку в списке.Так как единственныйтип указателей, которые поддерживаетVisual Basic —это ссылки наобъекты, тоячейки в связномсписке должныбыть объектами.
В классе, задающем ячейку, должна бытьопределенапеременнаяNextCell, которая указываетна следующуюячейку в списке.В нем такжедолжны бытьопределеныпеременные, содержащиеданные, с которымибудет работатьпрограмма. Этипеременныемогут бытьобъявлены какоткрытые (public)внутри класса, или класс можетсодержатьпроцедуры длячтения и записизначений этихпеременных.Например, всвязном спискес записями осотрудниках, в этих поляхмогут находитьсяимя сотрудника, номер социальногострахования, название должности, и т.д. Определениядля классаEmpCellмогут выглядетьпримерно так:

PublicEmpName As String
PublicSSN As String
PublicJobTitle As String
PublicNextCell As EmpCell

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

Dimtop_cell As EmpCell
Dimcell1 As EmpCell
Dimcell2 As EmpCell
Dimcell3 As EmpCell

‘Созданиеячеек.
Setcell1 = New EmpCell
cell1.EmpName= «Стивенс”
cell1.SSN= „123-45-6789“
cell1.JobTitle= „Автор“

Setcell2 = New EmpCell
cell2.EmpName= „Кэтс”
cell2.SSN= “123-45-6789»
cell2.JobTitle= «Юрист»

Setcell3 = New EmpCell
cell3.EmpName= «Туле”
cell3.SSN= „123-45-6789“
cell3.JobTitle= „Менеджер“

‘Соединитьячейки, образуясвязный список.
Setcell1.NextCell = cell2
Setcell2.NextCell = cell3
Setcell3.NextCell = Nothing

‘Сохранитьссылку на вершинусписка.
Settop_cell = cell1

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

На рис.2.2 показаносхематическоепредставлениеэтого связногосписка. Прямоугольникипредставляютячейки, а стрелки —ссылки на объекты.Маленькийперечеркнутыйпрямоугольникпредставляетзначение Nothing, котороеобозначаетконец списка.Имейте в виду, что top_cell,cell1и cell2 –это не настоящиеобъекты, а толькоссылки, которыеуказывают наних.
Следующийкод используетсвязный список, построенныйпри помощипредыдущегопримера дляпечати именсотрудниковиз списка. Переменнаяptrиспользуетсяв качествеуказателя наэлементы списка.Она первоначальноуказывает навершину списка.В коде используетсяцикл Doдля перемещенияptrпо списку дотех пор, покауказатель недойдет до концасписка. Во времякаждого цикла, процедурапечатает полеEmpNameячейки, на которуюуказывает ptr.Затем она увеличиваетptr, указывая наследующуюячейку в списке.В конце концов,ptrдостигает концасписка и получаетзначение Nothing, и цикл Doостанавливается.

Dimptr As EmpCell

Setptr = top_cell ‘ Начать свершины списка.
DoWhile Not (ptr Is Nothing)
‘Вывести полеEmpName этой ячейки.
Debug.Printptr.Empname
‘Перейти к следующейячейке в списке.
Setptr = ptr.NextCell
Loop

Послевыполнениякода вы получитеследующийрезультат:

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

@Рис.2.2. Связный список

=======28

Использованиеуказателя надругой объектназываетсякосвеннойадресацией(indirection), посколькувы используетеуказатель длякосвенногоманипулированияданными. Косвеннаяадресация можетбыть оченьзапутанной.Даже для простогорасположенияэлементов, такого, каксвязный список, иногда труднозапомнить, накакой объектуказываеткаждая ссылка.В более сложныхструктурахданных, указательможет ссылатьсяна объект, содержащийдругой указатель.Если есть несколькоуказателейи несколькоуровней косвеннойадресации, вылегко можетезапутатьсяв них
Длятого, чтобыоблегчитьпонимание, визложениииспользуютсяиллюстрации, такие как рис.2.2,(для сетевойверсии исключены, т.к. они многократноувеличиваютразмер загружаемогофайла) чтобыпомочь вамнаглядно представитьситуацию там, где это возможно.Многие из алгоритмов, которые используютуказатели, можно легкопроиллюстрироватьподобнымирисунками.Добавлениеэлементов ксвязному списку
Простойсвязный список, показанныйна рис. 2.2, обладаетнесколькимиважными свойствами.Во первых, можноочень легкодобавить новуюячейку в началосписка. Установимуказатель новойячейки NextCellна текущуювершину списка.Затем установимуказательtop_cellна новую ячейку.Рис. 2.3 соответствуетэтой операции.Код на языкеVisual Basic дляэтой операцииочень прост:
    продолжение
--PAGE_BREAK--
Setnew_cell.NextCell = top_cell
Settop_cell = new_cell

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

Сравнитеразмер этогокода и кода, который пришлосьбы написатьдля добавлениянового элементав начало списка, основанногона массиве, вкотором потребовалосьбы переместитьвсе элементымассива на однупозицию, чтобыосвободитьместо для новогоэлемента. Этаоперация сосложностьюпорядка O(N) можетпотребоватьмного времени, если списокдостаточнодлинный. Используясвязный список, моно добавитьновый элементв начало спискавсего за парушагов.

======29

Так желегко добавитьновый элементи в серединусвязного списка.Предположим, вы хотите вставитьновый элементпосле ячейки, на которуюуказываетпеременнаяafter_me.Установимзначение NextCellновой ячейкиравным after_me.NextCell.Теперь установимуказательafter_me.NextCellна новую ячейку.Эта операцияпоказана нарис. 2.4. Код наVisual Basicснова оченьпрост:

Setnew_cell.NextCell = after_me.NextCell
Setafter_me.NextCell = new_cell
Удалениеэлементов изсвязного списка
Удалитьэлемент извершины связногосписка так жепросто, как идобавить его.Просто установитеуказательtop_cellна следующуюячейку в списке.Рис. 2.5 соответствуетэтой операции.Исходный коддля этой операцииеще проще, чемкод для добавленияэлемента.

Settop_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этойячейки содержитсяссылка на другуюячейку. Посколькупервый объектуничтожается, то число ссылокна второй объектуменьшается.При этом счетчикссылок на второйобъект спискастановитсяравным нулю, поэтому системауничтожаети его.
Во времяуничтожениявторого объекта, система уменьшаетчисло ссылокна третий объект, и так далее дотех пор, покавсе объектыв списке небудут уничтожены.Когда в программеуже не будетссылок на объектысписка, можноуничтожитьи весь списокпри помощиединственногооператора Settop_cell = Nothing.

@Рис.2.6. Удалениеэлемента изсередины связногосписка

========31
Сигнальныеметки
Длядобавленияили удаленияэлементов изначала илисередины спискаиспользуютсяразличныепроцедуры.Можно свестиоба этих случаяк одному и избавитьсяот избыточногокода, если ввестиспециальнуюсигнальнуюметку (sentinel)в самом началесписка. Сигнальнуюметку нельзяудалить. Онане содержитданных и используетсятолько дляобозначенияначала списка.
Теперьвместо того, чтобы обрабатыватьособый случайдобавленияэлемента вначало списка, можно помещатьэлемент послеметки. Такимже образом, вместо особогослучая удаленияпервого элементаиз списка, простоудаляетсяэлемент, следующийза меткой.
Использованиесигнальныхметок пока невносит особыхразличий. Сигнальныеметки играютважную рольв гораздо болеесложных алгоритмах.Они позволяютобрабатыватьособые случаи, такие как началосписка, какобычные. Приэтом требуетсянаписать иотладить меньшекода, и алгоритмыстановятсяболее согласованнымии более простымидля понимания.
В табл.2.1 сравниваетсясложностьвыполнениянекоторыхтипичных операцийс использованиемсписков наоснове массивовсо «сборкоймусора» илисвязных списков.
Спискина основе массивовимеют однопреимущество: они используютменьше памяти.Для связныхсписков необходимодобавить полеNextCellк каждому элементуданных. Каждаяссылка на объектзанимает четыредополнительныхбайта памяти.Для очень большихмассивов этоможет потребоватьбольших затратпамяти.
ПрограммаLnkList1демонстрируетпростой связныйсписок с сигнальнойметкой. Введитезначение втекстовое полеввода, и нажмитена элемент всписке или наметку. Затемнажмите накнопку AddAfter (Добавитьпосле), и программадобавит новыйэлемент послеуказанного.Для удаленияэлемента изсписка, нажмитена элемент изатем на кнопкуRemove After(Удалить после).

@Таблица2.1. Сравнениесписков наоснове массивови связных списков

=========32
Инкапсуляциясвязных списков
ПрограммаLnkList1управляетсписком явно.Например, следующийкод показывает, как программаудаляет элементиз списка. Когдаподпрограмманачинает работу, глобальнаяпеременнаяSelectedIndexдает положениеэлемента, предшествующегоудаляемомуэлементу всписке. ПеременнаяSentinelсодержит ссылкуна сигнальнуюметку списка.

PrivateSub CmdRemoveAfter_Click()
Dimptr As ListCell
Dimposition As Integer

IfSelectedIndex

‘Найтиэлемент.
Setptr = Sentinel
position= SelectedIndex
DoWhile position > 0
position= position — 1
Setptr = ptr.nextCell
Loop

‘Удалить следуюшийэлемент.
Setptr.NextCell = ptr.NextCell.NextCell
NumItems= NumItems — 1

SelectItemSelectedIndex ‘ Сновавыбратьэлемент.
DisplayList
NewItem.SetFocus
EndSub

Чтобыупроститьиспользованиесвязного списка, можно инкапсулироватьего функциив классе. Этореализованов программеLnkList2. Она аналогичнапрограммеLnkList1, но используетдля управлениясписком классLinkedList.
КлассLinekedListуправляетвнутреннейорганизациейсвязного списка.В нем находятсяпроцедуры длядобавленияи удаленияэлементов, возвращениязначения элементапо его индексу, числа элементовв списке, и очисткисписка. Этоткласс позволяетобращатьсясо связнымсписком почтикак с массивом.
Этонамного упрощаетосновную программу.Например, следующийкод показывает, как программаLnkList2удаляет элементиз списка. Толькоодна строкав программев действительностиотвечает заудаление элемента.Остальныеотображаютновый список.Сравните этоткод с предыдущейпроцедурой:

Privatesub CmdRemoveAfter_Click()
Llist.RemoveAfterSelectedIndex

SelectedItemSelectedList ‘ Сновавыбратьэлемент.
DisplayList
NewItem.SetFocus
CmdClearList.Enabled
EndSub

=====33
Доступк ячейкам
КлассLinkedList, используемыйпрограммойLnkLst2, позволяетосновной программеиспользоватьсписок почтикак массив.Например, подпрограммаItem, приведеннаяв следующемкоде, возвращаетзначение элементапо его положению:

FunctionItem(ByVal position As Long) As Variant
Dimptr As ListCell

Ifposition m_NumItems Then
‘Выход за границы.ВернутьNULL.
Item= Null
ExitFunction
EndIf

‘Найтиэлемент.
Setptr = m_Sentinel
DoWhile position > 0
position= position — 1
Setptr = ptr.NextCell
Loop

Item= ptr.Value
EndFunction

Этапроцедурадостаточнопроста, но онане используетпреимуществасвязной структурысписка. Например, предположим, что программетребуетсяпоследовательноперебрать всеобъекты в списке.Она могла быиспользоватьподпрограммуItemдля поочередногодоступа к ним, как показанов следующемкоде:

Dimi As Integer

Fori = 1 To LList.NumItems
‘Выполнитькакие либодействия сLList.Item(i).
:
Nexti

Прикаждом вызовепроцедуры 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:

PublicSub MoveNext()
‘Если текущаяячейка не выбрана, ничего не делать.
IfNot (m_CurrentCell Is Nothing) Then _
Setm_CurrentCell = m_CurrentCell.NextCell
EndSub

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

LList.MoveFirst

DoWhile Not LList.EndOfList
‘Выполнитькакие либодействия надэлементомLList.Item(i).
:
LList.MoveNext
Loop

ПрограммаLnkList3используетэти новые методыдля управлениясвязным списком.Она аналогичнапрограммеLnkList2, но более эффективнообращаетсяк элементам.Для небольшихсписков, используемыхв программе, эта разницанезаметна. Дляпрограммы, которая обращаетсяко всем элементамбольшого списка, эта версиякласса LinkedListболее эффективна.Разновидностисвязных списков
Связныесписки играютважную рольво многих алгоритмах, и вы будетевстречатьсяс ними на протяжениивсего материала.В следующихразделах обсуждаютсянесколькоспециальныхразновидностейсвязных списков.Циклическиесвязные списки
Вместотого, чтобыустанавливатьуказательNextCellравным Nothing, можно установитьего на первыйэлемент списка, образуя циклическийсписок (circularlist), как показанона рис. 2.7.
Циклическиесписки полезны, если нужнообходить рядэлементов вбесконечномцикле. При каждомшаге цикла, программапросто перемещаетуказатель наследующуюячейку в списке.Допустим, имеетсяциклическийсписок элементов, содержащийназвания днейнедели. Тогдапрограмма моглабы перечислятьдни месяца, используяследующий код:

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

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

‘ Здесьнаходится коддля созданияи настройкисписка и т.д.
:
‘ Напечататькалендарь намесяц.

‘ first_day —это индексструктуры, содержащейдень неделидля
‘ первогодня месяца.Например, месяцможет начинаться
‘ впонедельник.

‘ num_days —число дней вмесяце.
PrivateSub ListMonth(first_day As Integer,num_days As Integer)
Dimptr As ListCell
Dimi As Integer

Setptr = top_cell
Fori = 1 to num_days
PrintFormat$(i) & ": " & ptr.Value
Setptr = ptr.NextCell
NextI
EndSub

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

PrivateSub PrintList(start_cell As Integer)
Dimptr As Integer

Setptr = start_cell
Do
Printptr.Value
Setptr = ptr.NextCell
LoopWhile Not (ptr Is start_cell)
EndSub

========36
Проблемациклическихссылок
Уничтожениециклическогосписка требуетнемного большевнимания, чемудаление обычногосписка. Есливы просто установитезначение переменнойtop_cellравным Nothing, то программане сможет большеобратитьсяк списку. Темне менее, посколькусчетчик ссылокпервой ячейкине равен нулю, она не будетуничтожена.На каждый элементсписка указываеткакой либодругой элемент, поэтому ни одиниз них не будетуничтожен.
Этопроблемациклическихссылок (circularreferencing problem).Так как ячейкиуказывают надругие ячейки, ни одна из нихне будет уничтожена.Программа неможет получитьдоступ ни кодной из них, поэтому занимаемаяими памятьбудет расходоватьсянапрасно дозавершенияработы программы.
Проблемациклическихссылок можетвстретитьсяне только вэтом случае.Многие сетисодержат циклическиессылки — дажеодиночнаяячейка, полеNextCellкоторой указываетна саму этуячейку, можетвызвать этупроблему.
Решениеее состоит втом, чтобы разбитьцепь ссылок.Например, выможете использоватьв своей программеследующий коддля уничтоженияциклическогосвязного списка:

Settop_cell.NextCell = Nothing
Settop_cell = Nothing

Перваястрока разбиваетцикл ссылок.В этот моментна вторую ячейкусписка не указываетни одна переменная, поэтому системауменьшаетсчетчик ссылокячейки до нуляи уничтожаетее. Это уменьшаетсчетчик ссылокна третий элементдо нуля, и соответственно, он также уничтожается.Этот процесспродолжаетсядо тех пор, покане будут уничтоженывсе элементысписка, кромепервого. Установказначения top_cellэлемента вNothingуменьшает егосчетчик ссылокдо нуля, и последняяячейка такжеуничтожается.Двусвязныесписки
Во времяобсуждениясвязных списковвы могли заметить, что большинствоопераций определялосьв терминахвыполнениячего либо послеопределеннойячейки в списке.Если заданаопределеннаяячейка, легкодобавить илиудалить ячейкупосле нее илиперечислитьидущие за нейячейки. Удалитьсаму ячейку, вставить новуюячейку передней или перечислитьидущие передней ячейки ужене так легко.Тем не менее, небольшоеизменениепозволит облегчитьи эти операции.
Добавимновое полеуказателя ккаждой ячейке, которое указываетна предыдущуюячейку в списке.Используя этоновое поле, можно легкосоздать двусвязныйсписок (doublylinked list), который позволяетперемещатьсявперед и назадпо списку. Теперьможно легкоудалить ячейку, вставить ееперед другойячейкой и перечислитьячейки в любомнаправлении.

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

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

КлассDoubleListCell, который используетсядля таких типовсписков, можетобъявлятьпеременныетак:

PublicValue As Variant
PublicNextCell As DoubleListCell
PublicPrevCell As DoubleListCell

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

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

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

PublicSub RemoveItem(ByVal target As DoubleListCell)
Dimafter_target As DoubleListCell
Dimbefore_target As DoubleListCell

Setafter_target = target.NextCell
Setbefore_target = target.PrevCell
Setafter_target.NextCell = after_target
Setafter_target.PrevCell = before_target
EndSub

SubAddAfter (new_Cell As DoubleListCell, after_me As DoubleListCell)
Dimbefore_me As DoubleListCell

Setbefore_me = after_me.NextCell
Setafter_me.NextCell = new_cell
Setnew_cell.NextCell = before_me
Setbefore_me.PrevCell =new_cell
Setnew_cell.PrevCell = after_me
EndSub
1 Or position >    продолжение
--PAGE_BREAK--
SubAddBefore(new_cell As DoubleListCell, before_me As DoubleListCell)
Dimafter_me As DoubleListCell

Setafter_me = before_me.PrevCell
Setafter_me.NextCell = new_cell
Setnew_cell.NextCell = before_me
Setbefore_me.PrevCell = new_cell
Setnew_cell.PrevCell = after_me
EndSub

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

Еслиснова взглянутьна рис. 2.9, вы увидите, что каждая парасоседних ячеекобразует циклическуюссылку. Этоделает уничтожениедвусвязногосписка немногоболее сложнойзадачей, чемуничтожениеодносвязныхили циклическихсписков. Следующийкод приводитодин из способовочистки двусвязногосписка. ВначалеуказателиPrevCellвсех ячеекустанавливаютсяравными Nothing, чтобы разорватьциклическиессылки. Это, посуществу, превращаетсписок в односвязный.Когда ссылкисигнальныхметок устанавливаютсяв Nothing, все элементыосвобождаютсяавтоматически, так же как и водносвязномсписке.

Dimptr As DoubleListCell
'Очистить указателиPrevCell, чтобы разорватьциклическиессылки.
Setptr = TopSentinel.NextCell
DoWhile Not (ptr Is BottomSentinel)
Setptr.PrevCell = Nothing
Setptr = ptr.NextCell
Loop
SetTopSentinel.NextCell = Nothing
SetBottomSentinel.PrevCell = Nothing

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

=============39
Потоки
В некоторыхприложенияхбывает удобнообходить связныйсписок не тольков одном порядке.В разных частяхприложениявам можетпотребоватьсявыводить списоксотрудниковпо их фамилиям, заработнойплате, идентификационномуномеру системысоциальногострахования, или специальности.
Обычныйсвязный списокпозволяетпросматриватьэлементы тольков одном порядке.ИспользуяуказательPrevCell, можно создатьдвусвязныйсписок, которыйпозволит перемещатьсяпо списку впереди назад. Этотподход можноразвить и дальше, добавив большеуказателейна структуруданных, позволяявыводить списокв другом порядке.
Наборссылок, которыйзадает какой либопорядок просмотра, называетсяпотоком (thread), а сам полученныйсписок —многопоточнымсписком (threadedlist). Не путайтеэти потоки спотоками, которыепредоставляетсистема WindowsNT.
Списокможет содержатьлюбое количествопотоков, хотя, начиная с какого томомента, игране стоит свеч.Применениепотока, упорядочивающегосписок сотрудниковпо фамилии, будет обосновано, если ваше приложениечасто используетэтот порядок, в отличие отрасположенияпо отчеству, которое врядли когда будетиспользоваться.
Некоторыерасположенияне стоит организовыватьв виде потоков.Например, поток, упорядочивающийсотрудниковпо полу, врядли целесообразенпотому, чтотакое упорядочениелегко получитьи без него. Длятого, чтобысоставитьсписок сотрудниковпо полу, достаточнопросто обойтисписок по любомудругому потоку, печатая фамилииженщин, а затемповторить обходеще раз, печатаяфамилии мужчин.Для получениятакого расположениядостаточновсего двухпроходов списка.
Сравнитеэтот случайс тем, когда выхотите упорядочитьсписок сотрудниковпо фамилии.Если списокне включаетпоток фамилий, вам придетсянайти фамилию, которая будетпервой в списке, затем следующуюи т.д. Это процесссо сложностьюпорядка O(N2), который намногоменее эффективен, чем сортировкапо полу со сложностьюпорядка O(N).
В общемслучае, заданиепотока можетбыть целесообразно, если его необходимочасто использовать, и если принеобходимостиполучить тотже порядокдостаточносложно. Потокне нужен, еслиего всегдалегко создатьзаново.
ПрограммаTreadsдемонстрируетпростой многопоточныйсписок сотрудников.Заполните поляфамилии, специальности, пола и номерасоциальногострахованиядля новогосотрудника.Затем нажмитена кнопку Add(Добавить), чтобыдобавить сотрудникак списку.
Программасодержит потоки, которые упорядочиваютсписок по фамилиипо алфавитуи в обратномпорядке, пономеру социальногострахованияи специальностив прямом и обратномпорядке. Выможете использоватьдополнительныекнопки длявыбора потока, в порядке которогопрограммавыводит список.На рис. 2.10 показаноокно программыThreadsсо спискомсотрудников, упорядоченнымпо фамилии.
КлассThreadedCell, используемыйпрограммойThreads, определяетследующиепеременные:

PublicLastName As String
PublicFirstName As String
PublicSSN As String
PublicSex As String
PublicJobClass As Integer
PublicNextName As TreadedCell ‘ Пофамилиив прямомпорядке.
PublicPrevName As TreadedCell ‘ Пофамилиив обратномпорядке.
PublicNextSSN As TreadedCell ‘ По номерув прямом порядке.
PublicNextJobClass As TreadedCell ‘ По специальностив прямом порядке.
PublicPrevJobClass As TreadedCell ‘ По специальностив обратномпорядке.

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

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

=====41

Следующийкод показывает, как классThreadedListвставляет новыйэлемент в потокиNextNameи PrevName.Так как этипотоки используютодин и тот жеключ — фамилии, программа можетобновлять иходновременно.

Dimptr As ThreadedCell
Dimnxt As ThreadedCell
Dimnew_cell As New ThreadedCell
Dimnew_name As String
Dimnext_name As String

'Записать значенияновой ячейки.
Withnew_cell
.LastName= LastName
.FirstName= FirstName
.SSN= SSN
•Sex= Sex
.JobClass= JobClass
EndWith

'Определитьместо новойячейки в потокеNextThread.
new_name= LastName & ", " & FirstName
Setptr = m_TopSentinel
Do
Setnxt = ptr.NextName
next_name= nxt.LastName & ", " & nxt.FirstName
Ifnext_name >= new_name Then Exit Do
Setptr = nxt
Loop

'Вставить новуюячейку в потокиNextName и divvName.
Setnew_cell.NextName = nxt
Setnew_cell.PrevName = ptr
Setptr.NextName = new_cell
Setnxt.PrevName = new_cell

Чтобытакой подходработал, программадолжна гарантировать, что значенияновой ячейкилежат междузначениямиметок. Например, если пользовательвведет в качествефамилии "~~", цикл выйдетза метку концасписка, т.к. "~~"идет после "~".Затем программааварийно завершитработу припопытке доступак значениюnxt.LastName, если nxtбыло установленоравным Nothing.

========42
Другиесвязные структуры
Используяуказатели, можно построитьмножестводругих полезныхразновидностейсвязных структур, таких как деревья, нерегулярныемассивы, разреженныемассивы, графыи сети. Ячейкаможет содержатьлюбое числоуказателейна другие ячейки.Например, длясоздания двоичногодерева можноиспользоватьячейку, содержащуюдва указателя, один на левогопотомка, и второй –на правого.Класс BinaryCellможет состоятьиз следующихопределений:

PublicLeftChild As BinaryCell
PublicRightChild As BinaryCell

На рис.2.11 показано дерево, построенноеиз ячеек такоготипа. В 6 главедеревья обсуждаютсяболее подробно.
Ячейкаможет дажесодержатьколлекцию илисвязный списокс указателямина другие ячейки.Это позволяетпрограммесвязать ячейкус любым числомдругих объектов.На рис. 2.12 приведеныпримеры другихсвязных структурданных. Вы такжевстретитепохожие структурыдалее, в особенностив 12 главе.Псевдоуказатели
Припомощи ссылокв Visual Basicможно легкосоздаватьсвязные структуры, такие как списки, деревья и сети, но ссылки требуютдополнительныхресурсов. Счетчикиссылок и проблемыс распределениемпамяти замедляютработу структурданных, построенныхс использованиемссылок.
Другойстратегией, которая частообеспечиваетлучшую производительность, является применениепсевдоуказателей(fake pointers).При этом программасоздает массивструктур данных.Вместо использованияссылок длясвязыванияструктур, программаиспользуетиндексы массива.Нахождениеэлемента вмассиве осуществляетсяв Visual Basicбыстрее, чемвыборка егопо ссылке наобъект. Этодает лучшуюпроизводительностьпри применениипсевдоуказателейпо сравнениюс соответствующимиметодами ссылокна объекты.
С другойстороны, применениепсевдоуказателейне столь интуитивно, как применениессылок. Этоможет усложнитьразработкуи отладку сложныхалгоритмов, таких как алгоритмысетей илисбалансированныхдеревьев.

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

========43

@Рис.2.12. Связные структуры

ПрограммаFakeListуправляетсвязным списком, используяпсевдоуказатели.Она создаетмассив простыхструктур данныхдля храненияячеек списка.ПрограммааналогичнапрограммеLnkList1, но используетпсевдоуказатели.
Следующийкод демонстрирует, как программаFakeListсоздает массивклеточныхструктур:

'Структураданных ячейки.
TypeFakeCell
ValueAs String
NextCellAs Integer
EndType

'Массив ячеексвязного списка.
GlobalCells(0 To 100) As FakeCell

'Сигнальнаяметка списка.
GlobalSentinel As Integer

Посколькупсевдоуказатели —это не ссылки, а просто целыечисла, программане может использоватьзначение Nothingдля маркировкиконца списка.ПрограммаFakeListиспользуетпостояннуюEND_OF_LIST, значение которойравно -32.767 дляобозначенияпустого указателя.
Дляоблегченияобнаружениянеиспользуемыхячеек, программаFakeListтакже используетспециальный«мусорный»список, содержащийнеиспользуемыеячейки. Следующийкод демонстрируетинициализациюпустого связногосписка. В немсигнальнаяметка NextCellпринимаетзначение END_OF_LIST.Затем она помещаетнеиспользуемыеячейки в «мусорный»список.

========44

'Связный списокнеиспользуемыхячеек.
GlobalTopGarbage As Integer

PublicSub InitializeList()
Dimi As Integer

Sentinel= 0
Cells(Sentinel).NextCell= END_OF_LIST

'Поместить всеостальныеячейки в «мусорный»список.
Fori = 1 To UBound (Cells) — 1
Cells(i).NextCell= i + 1
Nexti
Cells(UBound(Cells)).NextCell= END_OF_LIST
TopGarbage= 1
EndSub

Придобавленииэлемента ксвязному списку, программаиспользуетпервую доступнуюячейку из «мусорного»списка, инициализируетполе ячейкиValueи вставляетячейку в список.Следующий кодпоказывает, как программадобавляетэлемент послевыбранного:

PrivateSub CmdAddAfter_Click()
Dimptr As Integer
Dimposition As Integer
Dimnew_cell As Integer

'Найти местовставки.
ptr= Sentinel
position= Selectedlndex
DoWhile position > 0
position= position — 1
ptr= Cells(ptr).NextCell
Loop

'Выбрать новуюячейку из «мусорного»списка.
new_cell= TopGarbage
TopGarbage= Cells(TopGarbage).NextCell

'Вставить элемент.
Cells(new_cell).Value = NewItem.Text
Cells(new_cell).NextCell= Cells(ptr).NextCell
Cells(ptr).NextCell= new_cell
NumItems= NumItems + 1
DisplayList
SelectItemSelectedIndex + 1 ' Выбратьновыйэлемент.
NewItem.Text= ""
NewItem.SetFocus
CmdClearList.Enabled= True
EndSub

Послеудаления ячейкииз списка, программаFakeListпомещает удаленнуюячейку в «мусорный»список, чтобыее затем можнобыло легкоиспользовать:

PrivateSub CmdRemoveAfter_Click()
Dimptr As Integer
Dimtarget As Integer
Dimposition As Integer

IfSelectedIndex

'Найти элемент.
ptr= Sentinel
position= SelectedIndex
DoWhile position > 0
position= position — 1
ptr= Cells(ptr).NextCell
Loop

'Пропуститьследующийэлемент.
target= Cells(ptr).NextCell
Cells(ptr).NextCell= Cells(target).NextCell
NumItems= NumItems — 1

'Добавить удаленнуюячейку в «мусорный»список.
Cells(target).NextCell= TopGarbage
TopGarbage= target

SelectItemSelectedlndex ' Сновавыбратьэлемент.
DisplayList
CmdClearList.Enabled= NumItems > 0
NewItem.SetFocus
EndSub

Применениепсевдоуказателейобычно обеспечиваетлучшую производительность, но являетсяболее сложным.Поэтому имеетсмысл сначаласоздать приложение, используяссылки на объекты.Затем, если выобнаружите, что программазначительнуючасть временитратит наманипулированиессылками, выможете, еслинеобходимо, преобразоватьее с использованиемпсевдоуказателей.

=======45-46
Резюме
Используяссылки на объекты, вы можете создаватьгибкие структурыданных, такиекак связныесписки, циклическиесвязные спискии двусвязныесписки. Этисписки позволяютлегко добавлятьи удалять элементыиз любого местасписка.
Добавляядополнительныессылки к классуячеек, можнопревратитьдвусвязныйсписок в многопоточный.Развивая идальше этиидеи, можносоздаватьэкзотическиеструктурыданных, включаяразреженныемассивы, деревья, хэш таблицыи сети. Они подробноописываютсяв следующихглавах.

========47
Глава3. Стеки и очереди
В этойглаве продолжаетсяобсуждениесписков, начатоево 2 главе, иописываютсядве особыхразновидностисписков: стекии очереди. Стек —это список, вкотором добавлениеи удалениеэлементовосуществляетсяс одного и тогоже конца списка.Очередь — этосписок, в которомэлементы добавляютсяв один конецсписка, а удаляютсяс противоположногоконца. Многиеалгоритмы, включая некоторыеиз представленныхв следующихглавах, используютстеки и очереди.Стеки
Стек (stack) —это упорядоченныйсписок, в которомдобавлениеи удалениеэлементоввсегда происходитна одном концесписка. Можнопредставитьстек как стопкупредметов наполу. Вы можетедобавлятьэлементы навершину и удалятьих оттуда, ноне можете добавлятьили удалятьэлементы изсередины стопки.
Стекичасто называютсписками типапервый вошел —последний вышел(Last In First Outlist). По историческимпричинам, добавлениеэлемента в стекназываетсяпроталкиванием(pushing) элементав стек, а удалениеэлемента изстека — выталкиванием(popping) элементаиз стека.
Перваяреализацияпростого спискана основе массива, описанная вначале 2 главы, является стеком.Для отслеживаниявершины спискаиспользуетсясчетчик. Затемэтот счетчикиспользуетсядля вставкиили удаленияэлемента извершины списка.Небольшоеизменение —это новая процедураPop, которая удаляетэлемент изсписка, одновременновозвращая егозначение. Приэтом другиепроцедуры могутизвлекатьэлемент и удалятьего из списказа один шаг.Кроме этогоизменения, следующий кодсовпадает скодом, приведеннымво 2 главе.

DimStack() As Variant
DimStackSize As Variant

SubPush(value As Variant)
StackSize= StackSize + 1
ReDimPreserve Stack(1 To StackSize)
Stack(StackSize)= value
EndSub

SubPop(value As Variant)
value= Stack(StackSize)
StackSize= StackSize — 1
ReDimPreserve Stack(1 To StackSize)
EndSub

=====49

Всепредыдущиерассужденияо списках такжеотносятся кэтому видуреализациистеков. В частности, можно сэкономитьвремя, если неизменять размерпри каждомдобавленииили выталкиванииэлемента. ПрограммаSimListна описаннаяво 2 главе, демонстрируетэтот вид простойреализациисписков.
Программычасто используютстеки для храненияпоследовательностиэлементов, скоторыми программабудет работатьдо тех пор, покастек не опустеет.Действия содним из элементовможет приводитьк тому, что другиебудут проталкиватьсяв стек, но, в концеконцов, они всебудут удаленыиз стека. В качествепростого примераможно привестиалгоритм обращенияпорядка элементовмассива. Приэтом все элементыпоследовательнопроталкиваютсяв стек. Затемвсе элементывыталкиваютсяиз стека в обратномпорядке изаписываютсяобратно в массив.
    продолжение
--PAGE_BREAK--
DimList() As Variant
DimNumItems As Integer

'Инициализациямассива.
:

'Протолкнутьэлементы встек.
ForI = 1 To NumItems
PushList(I)
NextI

'Вытолкнутьэлементы изстека обратнов массив.
ForI = 1 To NumItems
PopList(I)
NextI

В этомпримере, длинастека можетмногократноизменятьсядо того, как, вконце концов, он опустеет.Если известнозаранее, насколькобольшим долженбыть массив, можно сразусоздать достаточнобольшой стек.Вместо измененияразмера стекапо мере того, как он растети уменьшается, можно отвестипод него памятьв начале работыи уничтожитьего после еезавершения.
Следующийкод позволяетсоздать стек, если заранееизвестен егомаксимальныйразмер. ПроцедураPopне изменяетразмер массива.Когда программазаканчиваетработу со стеком, она должнавызвать процедуруEmptyStackдля освобождениязанятой подстек памяти.

======50

ConstWANT_FREE_PERCENT = .1 ' 10% свободногопространства.
ConstMIN_FREE = 10 ' Минимальныйразмер.
GlobalStack() As Integer ' Стековыймассив.
GlobalStackSize As Integer ' Размерстековогомассива.
GlobalLastltem As Integer ' Индекспоследнегоэлемента.

SubPreallocateStack(entries As Integer)
StackSize= entries
ReDimStack(1 To StackSize)
EndSub

SubEmptyStack()
StackSize= 0
LastItem= 0
EraseStack ' Освободитьпамять, занятуюмассивом.
EndSub

SubPush(value As Integer)
LastItem= LastItem + 1
IfLastItem > StackSize Then ResizeStack
Stack(LastItem)= value
EndSub

SubPop(value As Integer)
value= Stack(LastItem)
LastItem= LastItem — 1
EndSub

SubResizeStack()
Dimwant_free As Integer

want_free= WANT_FREE_PERCENT * LastItem
Ifwant_free
StackSize= LastItem + want_free
ReDimPreserve Stack(1 To StackSize)
EndSub

Этотвид реализациистеков достаточноэффективенв Visual Basic.Стек не расходуетпонапраснупамять, и неслишком частоизменяет свойразмер, особенноесли сразуизвестно, насколькобольшим ондолжен быть.

=======51
Множественныестеки
В одноммассиве можносоздать двастека, поместиводин в началемассива, а другой —в конце. Длядвух стековиспользуютсяотдельныесчетчики длиныстека Top, и стеки растутнавстречу другдругу, как показанона рис. 3.1. Этотметод позволяетдвум стекамрасти, занимаяодну и ту жеобласть памяти, до тех пор, покаони не столкнутся, когда массивзаполнится.
К сожалению, менять размерэтих стековнепросто. Приувеличениимассива необходимосдвигать всеэлементы вверхнем стеке, чтобы выделятьпамять подновые элементыв середине. Приуменьшениимассива, необходимовначале сдвинутьэлементы верхнегостека, передтем, как менятьразмер массива.Этот методтакже сложномасштабироватьдля оперированияболее чем двумястеками.
Связныесписки предоставляютболее гибкийметод построениямножественныхстеков. Дляпроталкиванияэлемента встек, он помещаетсяв начало связногосписка. Длявыталкиванияэлемента изстека, удаляетсяпервый элементиз связногосписка. Так какэлементы добавляютсяи удаляютсятолько в началесписка, дляреализациистеков такоготипа не требуетсяприменениесигнальныхметок или двусвязныхсписков.
Основнойнедостатокприменениястеков на основесвязных списковсостоит в том, что они требуютдополнительнойпамяти дляхранения указателейNextCell.Для стека наоснове массива, содержащегоN элементов, требуется всего2*N байт памяти(по 2 байта нацелое число).Тот же стек, реализованныйна основе связногосписка, потребуетдополнительно4*N байт памятидля указателейNextCell, увеличиваяразмер необходимойпамяти втрое.
ПрограммаStackиспользуетнесколькостеков, реализованныхв виде связныхсписков. Используяпрограмму, можно вставлятьи выталкиватьэлементы изкаждого из этихсписков. ПрограммаStack2аналогичнаэтой программе, но она используеткласс LinkedListStackдля работы состеками.Очереди
Упорядоченныйсписок, в которомэлементы добавляютсяк одному концусписка, а удаляютсяс другой стороны, называетсяочередью(queue). Группалюдей, ожидающихобслуживанияв магазине, образует очередь.Вновь прибывшиеподходят сзади.Когда покупательдоходит доначала очереди, кассир егообслуживает.Из за их природы, очереди иногданазывают спискамитипа первыйвошел — первыйвышел (First In First Outlist).

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

=======52

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

GlobalQueue() As String ' Массивочереди.
GlobalQueuePront As Integer ' Началоочереди.
GlobalQueueBack As Integer ' Конецочереди.

SubEnterQueue(value As String)
ReDimPreserve Queue(QueueFront To QueueBack)
Queue(QueueBack)= value
QueueBack= QueueBack + 1
EndSub

SubLeaveQueue(value As String)
value= Queue(QueueFront)
QueueFront= QueueFront + 1
ReDimPreserve Queue (QueueFront To QueueBack — 1)
EndSub

К сожалению,Visual Basic непозволяетиспользоватьключевое словоPreserveв оператореReDim, если изменяетсянижняя границамассива. Дажеесли бы VisualBasic позволялвыполнениетакой операции, очередь приэтом «двигалась»бы по памяти.При каждомдобавленииили удаленииэлемента изочереди, границымассива увеличивалисьбы. После пропусканиядостаточнобольшого количестваэлементов черезочередь, ееграницы моглибы в конечномитоге статьслишком велики.
Поэтому, когда требуетсяувеличитьразмер массива, вначале необходимопереместитьданные в началомассива. Приэтом можетобразоватьсядостаточноеколичествосвободных ячеекв конце массива, так что увеличениеразмера массиваможет уже непонадобиться.В противномслучае, можновоспользоватьсяоператоромReDimдля увеличенияили уменьшенияразмера массива.
Как ив случае сосписками, можноповыситьпроизводительность, добавляя сразунесколькоэлементов приувеличенииразмера массива.Также можносэкономитьвремя, уменьшаяразмер массива, только когдаон содержитслишком многонеиспользуемыхячеек.
В случаепростого спискаили стека, элементыдобавляютсяи удаляютсяна одном егоконце. Еслиразмер спискаостается почтипостоянным, его не придетсяизменять слишкомчасто. С другойстороны, таккак элементыдобавляютсяна одном концеочереди, а удаляютсяс другого конца, может потребоватьсявремя от временипереупорядочиватьочередь, дажеесли ее размеростается неизменным.

=====53

ConstWANT_FREE_PERCENT = .1 ' 10% свободногопространства.
ConstMIN_FREE = 10 ' Минимумсвободныхячеек.
GlobalQueue() As String ' Массивочереди.
GlobalQueueMax As Integer ' Наибольшийиндексмассива.
GlobalQueueFront As Integer ' Началоочереди.
GlobalQueueBack As Integer ' Конецочереди.
GlobalResizeWhen As Integer ' Когда увеличитьразмер массива.

'При инициализациипрограммадолжна установитьQueueMax = -1
'показывая, чтопод массив ещене выделенапамять.

SubEnterQueue(value As String)
IfQueueBack > QueueMax Then ResizeQueue
Queue(QueueBack)= value
QueueBack= QueueBack + 1
EndSub

SubLeaveQueue(value As String)
value= Queue(QueueFront)
QueueFront= QueueFront + 1
IfQueueFront > ResizeWhen Then ResizeOueue
EndSub

SubResizeQueue()
Dimwant_free As Integer
Dimi As Integer
'Переместитьзаписи в началомассива.
Fori = QueueFront To QueueBack — 1
Queue(i- QueueFront) = Queue(i)
Nexti
QueueBack= QueueBack — QueuePront
QueueFront= 0

'Изменить размермассива.
want_free= WANT_FREE_PERCENT * (QueueBack — QueueFront)
Ifwant_free
Max= QueueBack + want_free — 1
ReDimPreserve Queue(0 To Max)

'Если QueueFront > ResizeWhen, изменитьразмер массива.
ResizeWhen= want_free
EndSub

Приработе с программой, заметьте, чтокогда вы добавляетеи удаляетеэлементы, требуетсяизменениеразмера очереди, даже если размерочереди почтине меняется.Фактически, даже при неоднократномдобавлениии удаленииодного элементаразмер очередибудет изменяться.
Имейтев виду, что прикаждом измененииразмера очереди, вначале всеиспользуемыеэлементы перемещаютсяв начало массива.При этом наизменениеразмера очередейна основе массивауходит большевремени, чемна изменениеразмера описанныхвыше связныхсписков и стеков.

=======54

ПрограммаArrayQ2аналогичнапрограммеArrayQ, но она используетдля управленияочередью классArrayQueue.Циклическиеочереди
Очереди, описанные впредыдущемразделе, требуетсяпереупорядочиватьвремя от времени, даже если размерочереди почтине меняется.Даже при неоднократномдобавлениии удаленииодного элементабудет необходимопереупорядочиватьочередь.
Еслизаранее известно, насколькобольшой можетбыть очередь, этого можноизбежать, создавциклическуюочередь (circularqueue). Идеязаключаетсяв том, чтобырассматриватьмассив очередикак будто онзаворачивается, образуя круг.При этом последнийэлемент массивакак бы идетперед первым.На рис. 3.2 изображенациклическаяочередь.
Программаможет хранитьв переменнойQueueFrontиндекс элемента, который дольшевсего находитсяв очереди. ПеременнаяQueueBackможет содержатьконец очереди, в который добавляетсяновый элемент.
В отличиеот предыдущейреализации, при обновлениизначений переменныхQueueFrontи QueueBack, необходимоиспользоватьоператор Modдля того, чтобыиндексы оставалисьв границахмассива. Например, следующий коддобавляетэлемент к очереди:

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

На рис.3.3 показан процессдобавлениянового элементак циклическойочереди, котораяможет содержатьчетыре записи.Элемент C добавляетсяв конец очереди.Затем конецочереди сдвигается, указывая наследующуюзапись в массиве.
Такимже образом, когда программаудаляет элементиз очереди, необходимообновлятьуказатель наначало очередипри помощиследующегокода:

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

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

=======55

@Рис.3.3. Добавлениеэлемента кциклическойочереди

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

@Рис.3.4. Удалениеэлемента изциклическойочереди

@Рис.3.5 Полная и пустаяциклическаяочереди

=========56

Следующийкод используетвсе эти методыдля управленияциклическойочередью:

Queue()As String ' Массивочереди.
QueueSizeAs Integer ' Наибольшийиндекс в очереди.
QueueFrontAs Integer ' Началоочереди.
QueueBackAs Integer ' Конецочереди.
NumInQueueAs Integer ' Число элементовв очереди.

SubNewCircularQueue(num_items As Integer)
QueueSize= num_items
ReDimQueue(0 To QueueSize — 1)
EndSub

SubEnterQueue(value As String)
'Если очередьзаполнена, выйти из процедуры.
'В настоящемприложениипотребуетсяболее сложныйкод.
IfNumInQueue >= QueueSize Then Exit Sub
Queue(QueueBack)= value
QueueBack= (QueueBack + 1) Mod QueueSize
NumInQueue= NumInQueue + 1
EndSub

SubLeaveQueue (value As String)
'Если очередьпуста, выйтииз процедуры.
'В настоящемприложениипотребуетсяболее сложныйкод.
IfNumInQueue
value= Queue (QueueFront)
QueueFront= (QueueFront + 1) Mod QueueSize
NumInQueue= NumInQueue — 1
EndSub

Также, как и в случаесо спискамина основе массивов, можно изменятьразмер массива, когда очередьполностьюзаполнитсяили если в массивебудет слишкоммного неиспользуемогопространства.Тем не менее, изменениеразмера циклическойочереди сложнее, чем изменитьразмер стекаили списка, основанногона массиве.
Когдаизменяетсяразмер массива, конец очередиможет не совпадатьс концом массива.Если простоувеличитьмассив, товставляемыеэлементы будутнаходитьсяв конце массива, так что онипопадут в серединусписка. На рис.3.6 показано, чтоможет произойтипри таком увеличениимассива.

===========57

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

PrivateSub EnterQueue(value As String)
IfNumInQueue >= QueueSize Then ResizeQueue
Queue(QueueBack)= value
QueueBack= (QueueBack + 1) Mod QueueSize
NumInQueue= NumInQueue + 1
EndSub

PrivateSub LeaveQueue(value As String)
IfNumInQueue
value= Queue (QueueFront)
QueueFront= (QueueFront + 1) Mod QueueSize
NumInQueue= NumInQueue — 1
IfNumInQueue
EndSub

SubResizeQueue()
Dimtemp() As String
Dimwant_free As Integer
Dimi As Integer
'Скопироватьэлементы вовременныймассив.
ReDimtemp(0 To NumInQueue — 1)
Fori = 0 To NumInQueue — 1
temp(i)= Queue((i + QueueFront) ModQueueSize)
Nexti

'Изменить размермассива.
want_free= WANT_FREE_PERCENT * NumInQueue
Ifwant_free
QueueSize= NumInQueue + want_free
ReDimQueue(0 To QueueSize — 1)
Fori = 0 To NumInQueue — 1
Queue(i)= temp(i)
Nexti
QueueFront= 0
QueueBack= NumInQueue

'Уменьшитьразмермассива, если NunInQueue
ShrinkWhen= QueueSize — 2 * want_free
'Не менять размернебольшихочередей. Этоможет вызвать
'проблемы с«ReDim temp(0 To NumInQueue — 1)» вышеи
'просто глупо!
IfShrinkWhen
EndSub

ПрограммаCircleQ демонстрируетэтот подходк реализациициклическойочереди. Введитестроку и нажмитекнопку Enter(Ввести) длядобавлениянового элементав очередь. Нажмитена кнопку Leave(Покинуть) дляудаления верхнегоэлемента изочереди. Программабудет принеобходимостиизменять размерочереди.
ПрограммаCircleQ2аналогичнапрограммеCircleQ, но она используетдля работы сочередью классCircleQueue.
Помните, что при каждомизмененииразмера очередив программе, она копируетэлементы вовременныймассив, изменяетразмер очереди, а затем копируетэлементы обратно.Эти дополнительныешаги делаютизменениеразмера циклическихочередей болеемедленным, чемизменениеразмера связныхсписков и стеков.Даже очередина основе массивов, в которых такжетребуютсядополнительныедействия дляизмененияразмера, нетребуют такогообъема работы.
С другойстороны, есличисло элементовв очереди несильно меняется, и если правильнозадать параметрыизмененияразмера, можетникогда непонадобитьсяменять размермассива. Дажеесли иногдаэто все такипридется делать, уменьшениечастоты этихизменений стоитдополнительныхусилий напрограммирование.Очередина основе связныхсписков
Совсемдругой подходк реализацииочередей состоитв использованиидвусвязныхсписков. Дляотслеживанияначала и концасписка можноиспользоватьсигнальныеметки. Новыеэлементы добавляютсяв очередь передметкой в концеочереди, а элементы, следующие заметкой началаочереди, удаляются.На рис. 3.7 показандвусвязныйсписок, которыйиспользуетсяв качествеочереди.

===========58-59

Добавлятьи удалять элементыиз двусвязногосписка легко, поэтому в этомслучае не потребуетсяприменятьсложных алгоритмовдля измененияразмера. Преимуществоэтого методатакже в том, что он интуитивнопонятнее посравнению сциклическойочередью наоснове массива.Недостатокего в том, чтодля указателейсвязного спискаNextCellи PrevCellтребуетсядополнительнаяпамять. В отношениизанимаемойпамяти очередина основе связныхсписков немногоменее эффективны, чем циклическиесписки.
ПрограммаLinkedQ работает сочередью припомощи двусвязногосписка. Введитестроку, нажмитена кнопку Enter, чтобы добавитьэлемент в конецочереди. Нажмитена кнопку Leaveдля удаленияэлемента изочереди.
ПрограммаLinkedQ2аналогичнапрограммеLinkedQ, но она используетдля управленияочередью классLinkedListqueue.Применениеколлекций вкачестве очередей
КоллекцииVisual Basicпредставляютсобой оченьпростую формуочереди. Программаможет использоватьметод Addколлекции длядобавленияэлемента вконец очереди, и метод Removeс параметром1 для удаленияпервого элементаиз очереди.Следующий кодуправляеточередью наоснове коллекций:
    продолжение
--PAGE_BREAK--
DimQueue As New Collection

PrivateSub EnterQueue(value As String)
Queue.Addvalue
EndSub

PrivateFunction LeaveQueue() As String
LeaveQueue= Queue.Item(1)
Queue.Remove1
ЕndFunction

@Рис.3.7. Очередь наоснове связногосписка

=======60

Несмотряна то, что этоткод очень прост, коллекции вдействительностине предназначеныдля использованияв качествеочередей. Онипредоставляютдополнительныевозможности, такие как ключиэлементов, иподдержка этихдополнительныхвозможностейделает коллекцииболее медленными, чем другиереализацииочередей. Темне менее, очередина основе коллекцийнастолькопросты, что онимогут бытьприемлемымвыбором дляприложений, в которыхпроизводительностьне являетсяпроблемой.
ПрограммаCollectQ демонстрируеточередь наоснове коллекций.Приоритетныеочереди
Каждыйэлемент вприоритетнойочереди (priorityqueue) имеетсвязанный сним приоритет.Если программенужно удалитьэлемент изочереди, онавыбирает элементс наивысшимприоритетом.Как хранятсяэлементы вприоритетнойочереди, неимеет значения, если программавсегда можетнайти элементс наивысшимприоритетом.
Некоторыеоперационныесистемы используюприоритетныеочереди дляпланированиязаданий. Воперационнойсистеме UNIX всепроцессы имеютразные приоритеты.Когда процессоросвобождается, выбираетсяготовый к исполнениюпроцесс с наивысшимприоритетом.Процессы сболее низкимприоритетомдолжны ждатьзавершенияили блокировки(например, приожидании внешнегособытия, такогокак чтениеданных с диска)процессов сболее высокимиприоритетами.
Концепцияприоритетныхочередей такжеиспользуетсяпри управленииавиаперевозками.Наивысшийприоритет имеютсамолеты, укоторых кончаетсятопливо вовремя посадки.Второй приоритетимеют самолеты, заходящие напосадку. Третийприоритет имеютсамолеты, находящиесяна земле, таккак они находятсяв более безопасномположении, чемсамолеты ввоздухе. Приоритетыизменяютсясо временем, так как у самолетов, которые пытаютсяприземлиться, в конце концов, закончитсятопливо.
Простойспособ организацииприоритетнойочереди —поместить всеэлементы всписок. Еслитребуетсяудалить элементиз очереди, можно найтив списке элементс наивысшемприоритетом.Чтобы добавитьэлемент в очередь, он помещаетсяв начало списка.При использованииэтого метода, для добавлениянового элементав очередь требуетсятолько одиншаг. Чтобы найтии удалить элементс наивысшимприоритетом, требуется O(N)шагов, еслиочередь содержитN элементов.
Немноголучше была бысхема с использованиемсвязного списка, в котором элементыбыли бы упорядоченыв прямом илиобратном порядке.Используемыйв списке классPriorityCellмог бы объявлятьпеременныеследующимобразом:

PublicPriority As Integer ' Приоритетэлемента.
PublicNextCell As PriorityCell ' Указательна следующийэлемент.
PublicValue As String ' Данные, нужныепрограмме.

Чтобыдобавить элементв очередь, нужнонайти его правильноеположение всписке и поместитьего туда. Чтобыупростить поискположенияэлемента, можноиспользоватьсигнальныеметки в началеи конце списка, присвоив имсоответствующиеприоритеты.Например, еслиэлементы имеютприоритетыот 0 до 100, можноприсвоить меткеначала приоритет101 и метке конца —приоритет  1.Приоритетывсех реальныхэлементов будутнаходитьсямежду этимизначениями.
На рис.3.8 показанаприоритетнаяочередь, реализованнаяна основе связногосписка.

=====61

@Рис.3.8. Приоритетнаяочередь наоснове связногосписка

Следующийфрагмент кодапоказываетядро этой процедурыпоиска:

Dimcell As PriorityCell
Dimnxt As PriorityCell

'Найти местоэлемента всписке.
cell= TopSentinel
nxt= cell.NextCell
DoWhile cell.Priority > new_priority
cell= nxt
nxt= cell.NextCell
Loop

'Вставить элементпосле ячейкив списке.
:

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

========63

Затративеще немногоусилий, можнопостроитьприоритетнуюочередь, в которойдобавлениеи удалениеэлемента потребуютпорядка O(log(N))шагов. Для оченьбольших очередей, ускорениеработы можетстоить этихусилий. Этоттип приоритетныхочередей используетструктурыданных в видепирамиды, которые такжеприменяютсяв алгоритмепирамидальнойсортировки.Пирамиды иприоритетныеочереди на ихоснове обсуждаютсяболее подробнов 9 главе.Многопоточныеочереди
Интереснойразновидностьюочередей являютсямногопоточныеочереди (multi headedqueues). Элементы, как обычно, добавляютсяв конец очереди, но очередьимеет несколькопотоков (frontend) или голов(heads). Программаможет удалятьэлементы излюбого потока.
Примероммногопоточнойочереди в обычнойжизни являетсяочередь клиентовв банке. Всеклиенты находятсяв одной очереди, но их обслуживаетнесколькослужащих.Освободившийсябанковскийработник обслуживаетклиента, которыйнаходится вочереди первым.Такой порядокобслуживаниякажется справедливым, посколькуклиенты обслуживаютсяв порядке прибытия.Он также эффективен, так как всеслужащие остаютсязанятыми дотех пор, покаклиенты ждутв очереди.
Сравнитеэтот тип очередис несколькимиоднопоточнымиочередями всупермаркете, в которых покупателине обязательнообслуживаютсяв порядке прибытия.Покупательв медленнодвижущейсяочереди, можетпрождать дольше, чем тот, которыйподошел позже, но оказалсяв очереди, котораяпродвигаетсябыстрее. Кассирытакже могутбыть не всегдазаняты, так каккакая либоочередь можетоказатьсяпустой, тогдакак в другихеще будут находитьсяпокупатели.
В общемслучае, многопоточныеочереди болееэффективны, чем несколькооднопоточныхочередей. Последнийвариант используетсяв супермаркетахпотому, чтотележки дляпокупок занимаютмного места.При использованиимногопоточнойочереди всемпокупателямпришлось быпостроитьсяв одну очередь.Когда кассиросвободится, покупателюпришлось быпереместитьсяс громоздкойтележкой ккассиру. С другойстороны, в банкепосетителямне нужно двигатьбольшие тележкидля покупок, поэтому онилегко могутуместитьсяв одной очереди.
Очередина регистрациюв аэропортуиногда представляютсобой комбинациюэтих двух ситуаций.Хотя пассажирыимеют с собойбольшое количествобагажа, в аэропортувсе таки используютсямногопоточныеочереди, приэтом приходитсяотводитьдополнительноеместо, чтобыпассажиры могливыстроитьсяв порядке очереди.
Многопоточнуюочередь простопостроить, используяобычную однопоточнуюочередь. Элементы, представляющиеклиентов, хранятсяв обычнойоднопоточнойочереди. Когдаагент (кассир, банковскийслужащий ит.д.) освобождается, первый элементв начале очередиудаляется ипередаетсяэтому агенту.Модельочереди
Предположим, что вы отвечаетеза разработкусчетчика регистрациидля новоготерминала ваэропорту ихотите сравнитьвозможностиодной многопоточнойочереди илинесколькиходнопоточных.Вам потребуетсякакая то модельповеденияпассажиров.Для этого примераможно сделатьследующиепредположения:

=====63

регистрация каждого пассажира занимает от двух до пяти минут;
при использовании нескольких однопоточных очередей, прибывающие пассажиры встают в самую короткую очередь;
скорость поступления пассажиров примерно неизменна.
ПрограммаHeadedQ моделируетэту ситуацию.Вы можете менятьнекоторыепараметрымодели, включаяследующие:
число прибывающих в течение часа пассажиров;
минимальное и максимальное затрачиваемое время;
число свободных служащих;
паузу между шагами программы в миллисекундах.
Привыполнениипрограммы, модель показываетпрошедшеевремя, среднееи максимальноевремя ожиданияпассажирамиобслуживания, и процент времени, в течение которогослужащие заняты.
Приэкспериментированиис различнымизначениямипараметров, вы заметитенескольколюбопытныхмоментов. Во-первых, для многопоточнойочереди среднееи максимальноевремя ожиданиябудет меньше.При этом, служащиетакже оказываютсянемного болеезагружены, чемв случае однопоточнойочереди.
Дляобоих типовочереди естьпорог, при которомвремя ожиданияпассажировзначительновозрастает.Предположим, что на обслуживаниеодного пассажиратребуется от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. Массивы
В этойглаве описаныструктурыданных в видемассивов. Спомощью VisualBasic вы можетелегко создаватьмассивы данныхстандартныхили определенныхпользователемтипов. Еслиопределитьмассив безграниц, затемможно изменятьего размер припомощи оператора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
Уравнениядля обратногопреобразования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 дляпреобразованиякоординат междудвумя массивами:

PrivateSub AtoB(ByVal I As Integer, ByVal J As Integer, X As Integer)
Dimtmp As Integer

IfI = J Then ' Незначащийэлемент.
X= -1
ExitSub
ElseIfI
tmp= I
I= J
J= tmp
EndIf
X= I * (I — 1) / 2 + J
EndSub

PrivateSub 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
EndSub

ПрограммаTriangиспользуетэти подпрограммыдля работы стреугольнымимассивами. Есливы нажмете накнопку A toB (Из A в B), программапометит элементыв массиве A ископирует этиметки в соответствующиеэлементы массиваB. Если вы нажметена кнопку B toA (Из B в A), программапометит элементыв массиве B, изатем скопируетметки в массивA.
ПрограммаTriangcиспользуеткласс TriangularArrayдля работы стреугольныммассивом. Пристарте программы, она записываетв объект TriangularArrayстроки, представляющиесобой элементымассива. Затемона извлекаети выводит наэкран элементымассива.Диагональныеэлементы
Некоторыепрограммыиспользуюттреугольныемассивы, которыевключают диагональныеэлементы A(I,I).В этом случаенеобходимовнести толькотри измененияв процедурыпреобразованияиндексов. ПроцедурапреобразованияAtoBне должна пропускатьслучаи с I=J, и должна добавлятьк Iединицу приподсчете индексамассива B.


Не сдавайте скачаную работу преподавателю!
Данный реферат Вы можете использовать для подготовки курсовых проектов.

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

Пишем реферат самостоятельно:
! Как писать рефераты
Практические рекомендации по написанию студенческих рефератов.
! План реферата Краткий список разделов, отражающий структура и порядок работы над будующим рефератом.
! Введение реферата Вводная часть работы, в которой отражается цель и обозначается список задач.
! Заключение реферата В заключении подводятся итоги, описывается была ли достигнута поставленная цель, каковы результаты.
! Оформление рефератов Методические рекомендации по грамотному оформлению работы по ГОСТ.

Читайте также:
Виды рефератов Какими бывают рефераты по своему назначению и структуре.

Сейчас смотрят :

Реферат Проблема чести и долга в романе Александра Пушкина Капитанская дочка
Реферат Определение поверхностного натяжения методом максимального давления в газовом пузырьке
Реферат Электромагнитное перемешивание в системе висмут–олово при бестигельной зонной плавке с индукционным нагревом
Реферат Инвестиционная политика фирмы
Реферат Основные фонды организации и эффективность их использования
Реферат Понятие Социология
Реферат Расчет сверхзвукового обтекания заостренных тел вращения
Реферат Византийские императоры
Реферат Особенности выставок и ярмарок как средства маркетинга
Реферат Налоги и налогообложение 2 Принципы налогообложения
Реферат Компьютерное сопровождение основных этапов читательской деятельности в процессе изучения эпичес
Реферат Николай Иванович Новиков
Реферат Clean Well Lit Place Essay Research Paper
Реферат Непрямой массаж сердца, искусственное дыхание
Реферат Финансовая устойчивость страховой компании на примере МСК АсСтра