Реферат: 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.



еще рефераты
Еще работы по информатике