Класс ThreadedList инкапсулирует многопоточный
список. Когда программа вызывает метод AddItem, список
обновляет свои потоки. Для каждого потока программа должна вставить элемент
в правильном порядке. Например, для того, чтобы вставить запись с фамилией «Смит»,
программа обходит список, используя поток NextName,
до тех пор, пока не найдет элемент с фамилией, которая должна следовать за «Смит».
Затем она вставляет в поток NextName новую запись
перед этим элементом.
При определении местоположения новых записей в потоке важную роль играют
сигнальные метки. Обработчик событий Class_Initialize
класса ThreadedList создает сигнальные метки на вершине
и в конце списка и инициализирует их указатели так, чтобы они указывали друг
на друга. Затем значение метки в начале списка устанавливается таким образом,
чтобы оно всегда находилось до любого значения реальных данных для всех потоков.
Например, переменная LastName может содержать строковые
значения. Пустая строка "" идет по алфавиту перед любыми действительными значениями
строк, поэтому программа устанавливает значение сигнальной метки
LastName в начале списка равным пустой строке.
Таким же образом Class_Initialize устанавливает
значение данных для метки в конце списка, превосходящее любые реальные значения
во всех потоках. Поскольку "~" идет по алфавиту после всех видимых символов
ASCII, программа устанавливает значение поля LastName
для метки в конце списка равным "~".
Присваивая полю LastName сигнальных меток значения
"" и "~", программа избавляется от необходимости проверять особые случаи, когда
нужно вставить новый элемент в начало или конец списка. Любые новые действительные
значения будут находиться между значениями LastValue
сигнальных меток, поэтому программа всегда сможет определить правильное положение
для нового элемента, не заботясь о том, чтобы не зайти за концевую метку и не
выйти за границы списка.
@Рис. 2.10. Программа Threads
=====41
Следующий код показывает, как класс ThreadedList
вставляет новый элемент в потоки NextName и
PrevName. Так как эти потоки используют один и тот
же ключ — фамилии, программа может обновлять их одновременно.
Dim ptr As ThreadedCell
Dim nxt As ThreadedCell
Dim new_cell As New ThreadedCell
Dim new_name As String
Dim next_name As String
' Записать значения новой ячейки.
With new_cell
.LastName = LastName
.FirstName = FirstName
.SSN = SSN
•Sex = Sex
.JobClass = JobClass
End With
' Определить место новой ячейки в потоке NextThread.
new_name = LastName & ", " & FirstName
Set ptr = m_TopSentinel
Do
Set nxt = ptr.NextName
next_name = nxt.LastName & ", " & nxt.FirstName
If next_name >= new_name Then Exit Do
Set ptr = nxt
Loop
' Вставить новую ячейку в потоки NextName и prevName.
Set new_cell.NextName = nxt
Set new_cell.PrevName = ptr
Set ptr.NextName = new_cell
Set nxt.PrevName = new_cell
Чтобы такой подход работал, программа должна гарантировать, что значения
новой ячейки лежат между значениями меток. Например, если пользователь введет
в качестве фамилии "~~", цикл выйдет за метку конца
списка, т.к. "~~" идет после "~".
Затем программа аварийно завершит работу при попытке доступа к значению
nxt.LastName, если
nxt было установлено равным Nothing.
========42
Другие связные структуры
Используя указатели, можно построить множество других полезных разновидностей
связных структур, таких как деревья, нерегулярные массивы, разреженные массивы,
графы и сети. Ячейка может содержать любое число указателей на другие ячейки.
Например, для создания двоичного дерева можно использовать ячейку, содержащую
два указателя, один на левого потомка, и второй – на правого. Класс
BinaryCell может состоять из следующих определений:
Public LeftChild As BinaryCell
Public RightChild As BinaryCell
На рис. 2.11 показано дерево, построенное из ячеек такого типа. В 6 главе
деревья обсуждаются более подробно.
Ячейка может даже содержать коллекцию или связный список с указателями на
другие ячейки. Это позволяет программе связать ячейку с любым числом других
объектов. На рис. 2.12 приведены примеры других связных структур данных. Вы
также встретите похожие структуры далее, в особенности в 12 главе.
Псевдоуказатели
При помощи ссылок в Visual Basic можно легко создавать связные структуры,
такие как списки, деревья и сети, но ссылки требуют дополнительных ресурсов.
Счетчики ссылок и проблемы с распределением памяти замедляют работу структур
данных, построенных с использованием ссылок.
Другой стратегией, которая часто обеспечивает лучшую производительность,
является применение псевдоуказателей (fake pointers). При этом программа создает
массив структур данных. Вместо использования ссылок для связывания структур,
программа использует индексы массива. Нахождение элемента в массиве осуществляется
в Visual Basic быстрее, чем выборка его по ссылке на объект. Это дает лучшую
производительность при применении псевдоуказателей по сравнению с соответствующими
методами ссылок на объекты.
С другой стороны, применение псевдоуказателей не столь интуитивно, как применение
ссылок. Это может усложнить разработку и отладку сложных алгоритмов, таких как
алгоритмы сетей или сбалансированных деревьев.
@Рис. 2.11. Двоичное дерево
========43
@Рис. 2.12. Связные структуры
Программа FakeList на диске с примерами управляет
связным списком, используя псевдоуказатели. Она создает массив простых структур
данных для хранения ячеек списка. Программа аналогична программе
LnkList1, но использует псевдоуказатели.
Следующий код демонстрирует, как программа FakeList
создает массив клеточных структур:
' Структура данных ячейки.
Type FakeCell
Value As String
NextCell As Integer
End Type
' Массив ячеек связного списка.
Global Cells(0 To 100) As FakeCell
' Сигнальная метка списка.
Global Sentinel As Integer
Поскольку псевдоуказатели — это не ссылки, а просто целые числа, программа
не может использовать значение Nothing для маркировки
конца списка. Программа FakeList использует постоянную
END_OF_LIST,
значение которой равно -32.767 для обозначения пустого указателя.
Для облегчения обнаружения неиспользуемых ячеек, программа
FakeList также использует специальный «мусорный» список,
содержащий неиспользуемые ячейки. Следующий код демонстрирует инициализацию
пустого связного списка. В нем сигнальная метка NextCell
принимает значение END_OF_LIST.
Затем она помещает неиспользуемые ячейки в «мусорный» список.
========44
' Связный список неиспользуемых ячеек.
Global TopGarbage As Integer
Public Sub InitializeList()
Dim i As Integer
Sentinel = 0
Cells(Sentinel).NextCell = END_OF_LIST
' Поместить все остальные ячейки в «мусорный» список.
For i = 1 To UBound (Cells) - 1
Cells(i).NextCell = i + 1
Next i
Cells(UBound(Cells)).NextCell = END_OF_LIST
TopGarbage = 1
End Sub
При добавлении элемента к связному списку, программа использует первую доступную
ячейку из «мусорного» списка, инициализирует поле ячейки
Value и вставляет ячейку в список. Следующий код показывает, как программа
добавляет элемент после выбранного:
Private Sub CmdAddAfter_Click()
Dim ptr As Integer
Dim position As Integer
Dim new_cell As Integer
' Найти место вставки.
ptr = Sentinel
position = Selectedlndex
Do While position > 0
position = position - 1
ptr = Cells(ptr).NextCell
Loop
' Выбрать новую ячейку из «мусорного» списка.
new_cell = TopGarbage
TopGarbage = Cells(TopGarbage).NextCell
' Вставить элемент.
Cells (new_cell).Value = NewItem.Text
Cells(new_cell).NextCell = Cells(ptr).NextCell
Cells(ptr).NextCell = new_cell
NumItems = NumItems + 1
DisplayList
SelectItem SelectedIndex + 1 ' Выбрать новый элемент.
NewItem.Text = ""
NewItem.SetFocus
CmdClearList.Enabled = True
End Sub
После удаления ячейки из списка, программа FakeList
помещает удаленную ячейку в «мусорный» список, чтобы ее затем можно было легко
использовать:
Private Sub CmdRemoveAfter_Click()
Dim ptr As Integer
Dim target As Integer
Dim position As Integer
If SelectedIndex < 0 Then Exit Sub
' Найти элемент.
ptr = Sentinel
position = SelectedIndex
Do While position > 0
position = position - 1
ptr = Cells(ptr).NextCell
Loop
' Пропустить следующий элемент.
target = Cells(ptr).NextCell
Cells(ptr).NextCell = Cells(target).NextCell
NumItems = NumItems - 1
' Добавить удаленную ячейку в «мусорный» список.
Cells(target).NextCell = TopGarbage
TopGarbage = target
SelectItem Selectedlndex ' Снова выбрать элемент.
DisplayList
CmdClearList.Enabled = NumItems > 0
NewItem.SetFocus
End Sub
Применение псевдоуказателей обычно обеспечивает лучшую производительность,
но является более сложным. Поэтому имеет смысл сначала создать приложение, используя
ссылки на объекты. Затем, если вы обнаружите, что программа значительную часть
времени тратит на манипулирование ссылками, вы можете, если необходимо, преобразовать
ее с использованием псевдоуказателей.
=======45-46
Резюме
Используя ссылки на объекты, вы можете создавать гибкие структуры данных,
такие как связные списки, циклические связные списки и двусвязные списки. Эти
списки позволяют легко добавлять и удалять элементы из любого места списка.
Добавляя дополнительные ссылки к классу ячеек, можно превратить двусвязный
список в многопоточный. Развивая и дальше эти идеи, можно создавать экзотические
структуры данных, включая разреженные массивы, деревья, хэш‑таблицы и сети.
Они подробно описываются в следующих главах.
========47
Глава 3. Стеки и очереди
В этой главе продолжается обсуждение списков, начатое во 2 главе, и описываются
две особых разновидности списков: стеки и очереди. Стек — это список, в котором
добавление и удаление элементов осуществляется с одного и того же конца списка.
Очередь — это список, в котором элементы добавляются в один конец списка, а
удаляются с противоположного конца. Многие алгоритмы, включая некоторые из представленных
в следующих главах книги, используют стеки и очереди.
Стеки
Стек (stack) — это упорядоченный список, в котором добавление и удаление
элементов всегда происходит на одном конце списка. Можно представить стек как
стопку предметов на полу. Вы можете добавлять элементы на вершину и удалять
их оттуда, но не можете добавлять или удалять элементы из середины стопки.
Стеки часто называют списками типа первый вошел — последний вышел (Last‑In‑First‑Out
list). По историческим причинам, добавление элемента в стек называется проталкиванием
(pushing) элемента в стек, а удаление элемента из стека — выталкиванием (popping)
элемента из стека.
Первая реализация простого списка на основе массива, описанная в начале 2
главы, является стеком. Для отслеживания вершины списка используется счетчик.
Затем этот счетчик используется для вставки или удаления элемента из вершины
списка. Небольшое изменение — это новая процедура Pop,
которая удаляет элемент из списка, одновременно возвращая его значение. При
этом другие процедуры могут извлекать элемент и удалять его из списка за один
шаг. Кроме этого изменения, следующий код совпадает с кодом, приведенным во
2 главе.
Dim Stack() As Variant
Dim StackSize As Variant
Sub Push(value As Variant)
StackSize = StackSize + 1
ReDim Preserve Stack(1 To StackSize)
Stack(StackSize) = value
End Sub
Sub Pop(value As Variant)
value = Stack(StackSize)
StackSize = StackSize - 1
ReDim Preserve Stack(1 To StackSize)
End Sub
=====49
Все предыдущие рассуждения о списках также относятся к этому виду реализации
стеков. В частности, можно сэкономить время, если не изменять размер при каждом
добавлении или выталкивании элемента. Программа SimList
на диске с примерами, описанная во 2 главе, демонстрирует этот вид простой реализации
списков.
Программы часто используют стеки для хранения последовательности элементов,
с которыми программа будет работать до тех пор, пока стек не опустеет. Действия
с одним из элементов может приводить к тому, что другие будут проталкиваться
в стек, но, в конце концов, они все будут удалены из стека. В качестве простого
примера можно привести алгоритм обращения порядка элементов массива. При этом
все элементы последовательно проталкиваются в стек. Затем все элементы выталкиваются
из стека в обратном порядке и записываются обратно в массив.
Dim List() As Variant
Dim NumItems As Integer
' Инициализация массива.
:
' Протолкнуть элементы в стек.
For I = 1 To NumItems
Push List(I)
Next I
' Вытолкнуть элементы из стека обратно в массив.
For I = 1 To NumItems
Pop List(I)
Next I
В этом примере, длина стека может многократно изменяться до того, как, в
конце концов, он опустеет. Если известно заранее, насколько большим должен быть
массив, можно сразу создать достаточно большой стек. Вместо изменения размера
стека по мере того, как он растет и уменьшается, можно отвести под него память
в начале работы и уничтожить его после ее завершения.
Следующий код позволяет создать стек, если заранее известен его максимальный
размер. Процедура Pop не изменяет размер массива.
Когда программа заканчивает работу со стеком, она должна вызвать процедуру
EmptyStack для освобождения занятой под стек памяти.
======50
Const WANT_FREE_PERCENT = .1 ' 10% свободного пространства.