Джулиан Бакнелл - Фундаментальные алгоритмы и структуры данных в Delphi
-------
Полный код класса TtdNodeManager можно найти на Web-сайте издательства, в разделе материалов. После выгрузки материалов отыщите среди них файл TDNdeMgr.pas.
Перед тем как вернуться к дальнейшим исследованиям односвязных списков, с которых мы начали наши рассуждения, отметим несколько недостатков класса TtdNodeManager. Первое, что следует отметить - это то, что метод FreeNode не проверяет, принадлежит ли освобождаемый узел к требуемому классу, т.е. находится ли он на странице, контролируемой классом. Это основной вопрос правильного функционирования класса. Если у класса есть узел, который не принадлежит к данному классу, он может иметь неверную длину (что в итоге может привести к перезаписи памяти) или может принадлежать к другому классу, который, возможно, очистит страницу, содержащую узел, и т.д. При отладке имеет смысл проводить проверку принадлежности к классу всех освобождаемых узлов. Реализация, содержащаяся в рамках сопровождающих книгу материалов, включает специальный код проверки при условии, что модель будет компилироваться с использованием утверждений.
Вторая проблема вызвана тем, что мы легко можем удалить экземпляр диспетчера узлов до удаления объектов, которые эти узлы используют. Это приведет к возникновению неизвестных ошибок. Только достаточная внимательность во время программирования может нас избавить от такого рода ошибок.
(Кстати, в качестве простого доказательства того, что мы не зря потеряли время на реализацию диспетчера узлов, можно сказать, что тесты по распределению и освобождению одного миллиона узлов показали, что диспетчер узлов работает в 3-4 раза быстрее, чем диспетчер кучи Delphi.)
Класс односвязного списка
Перед тем как приступить к реализации класса TtdSingleLinkList для представления односвязного списка, рассмотрим несколько вводных замечаний.
Начнем с самого начала. Как уже упоминалось, было бы очень удобно использовать связный список, не беспокоясь о его узлах. Хотелось бы, чтобы класс связного списка мог работать с любыми типами указателей, подобно классу TList. Для получения доступа к элементам связного списка было бы желательно использовать индекс (несмотря на то что это может негативно сказаться на быстродействии), но еще лучше было бы использовать терминологию баз данных. Так, в связном списке можно использовать курсор, который указывает на "текущий" элемент списка. Тогда можно было бы написать методы для позиционирования курсора перед любым элементом списка, перемещения курсора на следующий элемент, вставки и удаления элемента в позиции курсора и т.д. Поскольку мы создаем связный список в виде класса, мы можем работать с родительским объектом текущего элемента, что позволит запрограммировать метод Insert так, как он реализован в TList (т.е. за счет перемещения текущего элемента и всех последующих элементов на одну позицию и вставки в освободившееся место нового элемента). Аналогично можно реализовать и метод Delete.
Интерфейс класса TtdSingleLinkList выглядит следующим образом:
Листинг 3.7. Класс TtdSingleLinkList
TtdSingleLinkList = class private
FCount : longint;
FCursor : PslNode;
FCursorIx: longint;
FDispose : TtdDisposeProc;
FHead : PslNode;
FNanie : TtdNameString;
FParent : PslNode;
protected
function sllGetItem(aIndex : longint): pointer;
procedure sllSetItem(aIndex : longint; aItem : pointer);
procedure sllError(aErrorCode : integer;
const aMethodName : TtdNameString);
class procedure sllGetNodeManager;
procedure sllPositionAtNth(aIndex : longint);
public
constructor Create(aDispose : TtdDisposeProc);
destructor Destroy; override;
function Add(aItem : pointer): longint;
procedure Clear;
procedure Delete(aIndex : longint);
procedure DeleteAtCursor;
function Examine : pointer;
function First : pointer;
function IndexOf(aItem : pointer): longint;
procedure Insert(aIndex : longint; aItem : pointer);
procedure InsertAtCursor(aItem : pointer);
function IsAfterLast : boolean;
function IsBeforeFirst : boolean;
function IsEmpty : boolean;
function Last : pointer;
procedure MoveBeforeFirst;
procedure MoveNext;
procedure Remove(aItem : pointer);
procedure Sort(aCompare : TtdCompareFunc);
property Count : longint read FCount;
property Items[aIndex : longint] : pointer read sllGetItem write sllSetItem; default;
property Name : TtdNameString read FName write FName;
end;
Несмотря на то что названия методов соответствуют стандарту TList, появилось несколько новых методов. Метод MoveBeforeFirst помещает курсор перед всеми элементами связного списка. IsBeforeFirst и IsAfterLast возвращают True, если курсор находится, соответственно, перед всеми элементами или после всех элементов списка. Метод MoveNext перемещает курсор на следующий элемент списка. Свойство Items аналогично соответствующему свойству списка TList: элементы нумеруются от 0 до Count-1.
Конструктор Create проверяет, создан ли экземпляр диспетчера узлов, а затем распределяет память для узла, который будет фиктивным начальным узлом. Затем курсор помещается перед всеми узлами (поскольку в списке еще нет узлов, это совсем несложно). Деструктор Destroy очищает связный список и освобождает фиктивный начальный узел, выделенный конструктором Create.
Листинг 3.8. Конструктор и деструктор класса TtdSingleLinkList
constructor TtdSingleLinkList.Create(aDispose : TtdDisposeProc);
begin
inherited Create;
{сохранить процедуру удаления}
FDispose :=aDispose;
{получить диспетчер узлов}
s 11 GetNodeManager;
{распределить память под начальный узел}
FHead := PslNode (SLNodeManager.AllocNode);
FHead^.slnNext := nil;
FHead^.slnData := nil;
{установить курсор}
MoveBeforeFirst;
end;
destructor TtdSingleLinkList.Destroy;
begin
{удалить все узлы, включая начальный фиктивный узел}
Clear;
SLNodeManager.FreeNode(FHead);
inherited Destroy;
end;
Особый интерес здесь представляет тот факт, что связный список организован таким образом, что для всех экземпляров класса TtdSingleLinkList создается только один диспетчер узлов. Все экземпляры пользуются одним и тем же диспетчером. Можно было бы запрограммировать, чтобы каждый класс создавал свой диспетчер, но это бы означало использование большого дополнительного объема для экземпляра класса. Таким образом, учитывая то, что в приложении, в котором имеется один связный список, как правило, есть несколько списков, было решено ввести переменную класса. Но во всех этих рассуждениях присутствует один недостаток: Delphi не поддерживает переменные класса. Поэтому в коде мы имитируем такую переменную, объявив ее как глобальную в разделе implementation модуля. Если вы просмотрите содержимое файла TDLnkLst.pas, то найдете следующее объявление:
var
SLNodeManager : TtdNodeManager;
Все методы класса односвязного списка можно разбить на две категории: методы, действующие по последовательной схеме (MoveBeforeFirst, InsertAtCursor и т.д.), и методы, которые работают со списком как с массивом (свойство Items, методы Delete, IndexOf и т.д.). Рассмотрим сначала методы первой группы, поскольку мы уже говорили о принципе их работы в начале главы при описании связных списков. Для упрощения реализации мы не только храним курсор (т.е. указатель на текущий узел) в объекте, но и родительский объект курсора (т.е. указатель на родительский объект текущего курсора). Такая схема позволяет упростить методы вставки и удаления элементов.
Листинг 3.9. Стандартные операции со связным списком для класса TtdSingleLinkList
procedure TtdSingleLinkList.Clear;
var
Temp : PslNode;
begin
{удалить все узлы, за исключением начального; при возможности освободить все данные}
Temp := FHead^.slnNext;
while (Temp <> nil) do
begin
FHead^.slnNext := Temp^.slnNext;
if Assigned(FDispose) then
FDispose(Temp^.slnData);
SLNodeManager.FreeNode(Temp);
Temp := FHead^.slnNext;
end;
FCount := 0;
MoveBeforeFirst;
end;
procedure TtdSingleLinkList.DeleteAtCursor;
begin
if (FCursor = nil) or (FCursor = FHead) then
sllError(tdeListCannotDelete, 'Delete');
{удалить все элементы}
if Assigned(FDispose) then
FDispose(FCursor^.slnData);
{удалить ссылки на узел и удалить сам узел}
FParent^.slnNext := FCursor^.slnNext;
SLNodeManager.FreeNode(FCursor);
FCursor := FParent^.slnNext;
dec(FCount);
end;
function TtdSingleLinkList.Examine : pointer;
begin
if (FCursor = nil) or (FCursor = FHead) then
sllError(tdeListCannotExamine, 'Examine');
{вернуть данные с позиции курсора}
Result := FCursor^.slnData;
end;
procedure TtdSingleLinkList.InsertAtCursor(aItem : pointer);
var
NewNode : PslNode;
begin
{убедиться, что вставка производится не перед первой позицией; если курсор находится перед первой позицией, переместить его на одну позицию вперед}
if (FCursor = FHead) then
MoveNext;
{распределить новый узел и вставить его в позицию курсора}
NewNode := PslNode (SLNodeManager.AllocNode);
NewNode^.slnData := aItem;
NewNode^.slnNext := FCursor;
FParent^.slnNext := NewNode;
FCursor := NewNode;
inc(FCount);
end;
function TtdSingleLinkList.IsAfterLast : boolean;
begin
Result := FCursor;
nil;
end;
function TtdSingleLinkList.IsBeforeFirst : boolean;
begin
Result := FCursor = FHead;
end;
function TtdSingleLinkList.IsEmpty : boolean;
begin
Result := (Count = 0);
end;
procedure TtdSingleLinkList.MoveBeforeFirst;
begin
{установить курсор на начальный узел}
FCursor := FHead;
FParent := nil;
FCursorIx := -1;
end;
procedure TtdSingleLinkList.MoveNext;
begin
{переместить курсор по его указателю Next, игнорировать попытку выхода за конечный узел списка}
if (FCursor <> nil) then begin
FParent := FCursor;
FCursor := FCursor^.slnNext;
inc(FCursorIx);
end;
end;
Вы, возможно, обратили внимание, что некоторые из приведенных методов пользуются полем объекта FCursorIx. Именно это поле позволяет обеспечить высокую эффективность методов, основанных на использовании индекса, поскольку в нем хранится индекс курсора (при этом первый узел имеет индекс 0, точно так же как в TList). Значение поля используется методом ellPositionAtNth, который оптимальным образом перемещает курсор в позицию с указанным индексом.