Джулиан Бакнелл - Фундаментальные алгоритмы и структуры данных в Delphi
Листинг 2.28. Реализация постоянных массивов с помощью файлового потока
constructor TtdRecordFile.Create(const aFileName : string;
aMode : word;
aRecordLength : integer);
begin
FStream := TFileStream.Create(aFileName, aMode);
inherited Create(FStream, aRecordLength);
FFileName := aFileName;
Mode := aMode;
end;
destructor TtdRecordFile.Destroy;
begin
inherited Destroy;
FStream.Free;
end;
procedure TtdRecordFile.Flush;
{$IFDEF Delphi1}
var
DosError : word;
Handle : THandle;
begin
Handle := FStream.Handle;
asm
mov ah, $68
mov bx, Handle
call D0S3Call
jc @@Error
xor ax, ax
@6Error:
mov DosError, ax
end;
if (DosError <> 0) then
rsError(tdeRSFlushError, 'Flush', DosError)
end;
{$ENDIF}
{$IFDEF Delphi2Plus}
begin
if not FlushFileBuffers (FStream.Handle) then
rsError(tdeRSFlushError, 'Flush', GetLastError)
end;
{$ENDIF}
В приведенном коде присутствует перекрытый метод Flush, который сбрасывает данные в дескриптор, связанный с файловым потоком, содержащим постоянный массив. Реализации для Delphi1 и для 32-битных версий будут отличаться, поскольку процесс сброса данных в дескриптор в этих версиях различен.
Полный код класса TtdRecordStream можно найти на Web-сайте издательства, в разделе материалов. После выгрузки материалов отыщите среди них файл TDRecFil.pas.
Резюме
Эта глава была посвящена массивам - одной из фундаментальных структур данных. Были описаны их достоинства (доступ к отдельным элементам составляет O(1), поддерживается локальность ссылок) и недостатки (вставка и удаление элементов относятся к операциям класса О(n)). Приведена реализация класса массива TtdRecordList. Затем был подробно рассмотрен стандартный класс TList и его простой дочерний класс TtdObjectList.
Кроме того, мы познакомились с реализацией постоянных массивов в форме потока записей. Был приведен пример реализации класса постоянных массивов, TtdRecordStream, который позволяет выполнять чтение, запись и удаление отдельных записей.
Глава 3. Связные списки, стеки и очереди
Как и массивы, связные списки представляют собой универсальную структуру данных, широко используемую многими программистами. Однако, в отличие от массивов, связные списки не входят в состав стандартного языка Object Pascal. Тем не менее, в Object Pascal создать связный список достаточно просто. Все что для этого нужно - наличие в составе языка указателя, хотя фактически могут использоваться и классы или объекты.
На основе связных списков можно легко организовать стеки и очереди - еще две простые, но эффективные структуры данных. Несмотря на то что они, на первый взгляд, не имеют ничего общего со связными списками, их можно написать на базе односвязных списков. И, как мы увидим чуть позже, иногда удобнее реализовать стеки и очереди на базе массивов, а не связных списков.
Начнем наше рассмотрение со связного списка и операций, которые такой список должен поддерживать.
Односвязные списки
По своей сути связный список (linked list) представляет собой цепочку элементов или объектов с некоторыми описаниями (обычно называемых узлами). При этом каждый элемент содержит указатель, указывающий на следующий элемент в списке. Такая структура данных называется односвязным списком (singly linked list) - каждый элемент имеет только одну ссылку или указатель на следующий элемент. Сам список начинается с первого узла, от которого путем последовательных переходов по ссылкам можно обойти все остальные узлы. Обратите внимание, что определение связного списка отличается от определения массива, для которого следующий элемент находится в памяти рядом с предыдущим. В связном списке элементы могут быть разбросаны по разным местам памяти, а их порядок определяется ссылками.
Рисунок 3.1. Односвязный список
А каким образом помечается конец списка? Самый простой способ - установить указатель ссылки в последнем элементе списка равным nil. Это будет означать, что следующий элемент отсутствует. Второй способ - ввести специальный узел, называемый конечным узлом, и установить так, чтобы ссылка последнего узла указывала на этот узел. И третий способ - установить так, чтобы ссылка последнего узла указывала на первый элемент. В этом случае мы получим круговой связный список.
А теперь рассмотрим, чем же связный список отличается от массива. Первое, что нужно отметить, - размер связного списка можно не устанавливать. Для массива нам всегда было нужно заранее знать, сколько элементов будет в нем храниться (чтобы можно было статически распределить непрерывный участок памяти) или разработать некоторую схему расширения массива (или его сокращения), чтобы массив мог разместить большее (или меньшее) количество элементов. В связном списке каждый узел является отдельным элементом. И в простых случаях распределение памяти под каждый узел выполняется отдельно. При необходимости добавления в список нового элемента под него распределяется память, а затем элемент просто на него устанавливается ссылка из списка. При удалении узла нужно всего лишь удалить ссылки на него и освободить занимаемую им память.
Хорошо. Если связный список настолько удобен, почему бы его не использовать вместо массива? В чем состоят его недостатки? Первый, хотя и незначительный, состоит в том, что каждый элемент связного списка должен содержать указатель на следующий элемент. Таким образом, чтобы вставить элемент в список, его реальный размер необходимо увеличить на размер указателя (в настоящее время это 4 байта).
Хуже то, что память под каждый узел распределяется отдельно. Сравним эту ситуацию с аналогичной ситуацией для массива. Распределение памяти под n элементов массива, фактически, представляет собой операцию класса O(1): все элементы должны находится в одном непрерывном блоке памяти, поэтому одновременно распределяется целый блок. (Нужно помнить, что память для элементов массивов не обязательно должна распределяться из кучи. Массивы могут представлять собой, например, локальные переменные в стеке.) Для связного списка память под узлы распределяется отдельно, следовательно, это операция класса O(n). Даже если не учитывать быстродействие, подобное поведение может привести к фрагментации кучи.
Самым большим недостатком связного списка является получение доступа к некоторому элементу n. В массиве доступ к n-ному элементу требует проведения простых арифметических вычислений, поскольку все элементы содержатся в одном непрерывном блоке памяти. С другой стороны, в списке получение доступа к элементу n требует прохождения по ссылкам от первого элемента до n-ного. Другого метода доступа не существует, мы всегда должны следовать по ссылкам. (Обратите внимание, что можно применить определенные хитрости, например, хранить элемент и его позицию в рамках списка в кэш-памяти. В таком случае можно определять целесообразность начала прохождения списка с его первого элемента или с элемента, хранящегося в кэш-памяти.)
Узлы связного списка
Перед началом описания операций со связным списком давайте рассмотрим, как каждый узел списка будет представляться в памяти. Знание структуры узла позволит нам более детально рассматривать основные операции со связными списком. Структура узла списка, не использующего классы и объекты, выглядит следующим образом:
type
PSimpleNode = ^TSimpleNode;
TSimpleNode = record
Next : PSimpleNode;
Data : SomeDataType;
end;
Тип PSimpleNode представляет собой указатель на запись TSimpleNode, поле Next которой содержит ссылку на точно такой же узел, а поле Data - сами данные. В приведенном примере тип данных узла задан как SomeDataType. Для перехода по ссылке нужно написать примерно следующий код:
var
NextNode, CurrentNode : PSimpleNode;
begin
• • •
NextNode := CurrentNode^.Next;
Создание односвязного списка
Это тривиальная задача. В самом простом случае первый узел в связном списке описывает весь список. Первый узел иногда называют головой списка.
var
MyLinkedList : PSimpleNode;
Если MyLinkedList содержит nil, списка еще нет. Таким образом, это начальное значение связного списка.
{инициализация связного списка}
MyLinkedList := nil;
Вставка и удаление элементов в односвязном списке
А каким образом можно вставить новый элемент в связный список? Или удалить? Оказывается, что для выполнения этих операций требуется выполнить небольшую работу с указателями.
Для односвязного списка существует только один вариант вставки - после заданного элемента списка. Нужно установить так, чтобы указатель Next нашего нового узла указывал на узел после заданного, а указатель Next заданного узла - на наш новый узел. В коде это выглядит следующим образом:
var
GivenNode, NewNode : PSimpleNode;
begin
• • •
New(NewNode);
.. задать значение поля Data..
NewNode^.Next := GivenNode^.Next;
GivenNode^.Next := NewNode;
Рисунок 3.2. Вставка нового узла в односвязный список
Аналогично, для удаления простейшим вариантом является удаление элемента, находящегося после заданного узла. В этом случае мы устанавливаем, чтобы указатель Next заданного узла указывал на узел, расположенный после удаляемого. После этого удаляемый узел уже выделен из списка и может быть освобожден. В коде это выглядит следующим образом: