Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Демидов Основы программирования в примерах на языке ПАСЦАЛ 2010

.pdf
Скачиваний:
128
Добавлен:
16.08.2013
Размер:
1.28 Mб
Скачать

Таким образом, новая запись станет первой в списке. Применяя эту операцию многократно, можно получить список любой длины. Очевидно, начальным значением Head должен быть NIL.

Пример подпрограммы для добавления записи выглядит следующим образом:

procedure Add2List(var Head: pphone_rec; fio, phone, email: string);

var

p: pphone_rec; begin

New(p); p^.fio := fio;

p^.phone := phone; p^.email := email; p^.next := Head; Head := p;

end;

Нужно отметить, что по завершении работы процедуры память, выделенная под переменную p, будет возвращена системе, однако выделенная динамическая память останется, причем адрес выделенного участка будет помещен в переменную Head.

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

procedure PrintList(Head: pphone_rec); var

p: pphone_rec; begin

p := Head;

while p <> NIL do begin writeln(p^.fio, p^.phone, p^.email);

p := p^.next; // переход к следующей записи end;

end;

Аналогичным образом можно посчитать число записей в списке. По завершении работы со списком выделенную под него динамическую память необходимо вернуть системе. Здесь следует остерегаться потери указателя на начало списка. Например, вызов Dis-

151

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

procedure DisposeList(var Head: pphone_rec); var

p: pphone_rec; begin

p := Head;

while p <> NIL do begin

//запоминаем указатель на следующую запись

Head := Head^.next;

//возвращаем память для первой записи

Dispose(p);

//переходим к остатку списка

p := Head; end;

end;

Задание. Реализовать процедуру поиска записи в списке по фамилии:

function Search(Head: pphone_rec; fio: string): pphone_rec; var p: pphone_rec;

begin

Search := NIL; p := Head;

while p <> NIL do begin

if p^.fio = fio then begin

Search := p;

break; // прекращение поиска, когда запись найдена end;

p := p^.next; end;

end;

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

152

type

// полезная часть, записываемая в типизированный файл phone_rec = record

fio: string[60]; phone: string[20]; e_mail: string[50];

end;

// служебная часть для работы с динамическими списками pNode = ^Node;

Node = record pr: phone_rec; next: pNode;

end;

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

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

type

pNode = ^Node; Node = record

data: pointer; next: pNode;

end;

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

153

Глава 15. Динамические структуры данных

Организация стеков и очередей на линейных списках

Рассмотрим следующую структуру данных:

type

pNode = ^Node; Node = record

data: pointer; next: pNode;

end;

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

procedure Push(var Stack: pNode; data: pointer) – добавление элемента в стек;

function Pop(var Stack: pNode): pointer – извлечение элемента из стека (поскольку стек работает по принципу LIFO «Последним пришел, первым вышел», то функция Pop должна извлекать последний добавленный элемент);

function Length(Stack: pNode): boolean – текущая длина стека; procedure Clear(var Stack: pNode) – инициализация стека (если в стеке находились какие-либо данные, то они должны удаляться). Пусть процедура Push добавляет элемент в начало списка. Тогда

// добавление элемента в стек

procedure Push(var Stack: pNode; data: pointer); var

n: pNode; begin

New(n);

n^.data := data; n^.next := Stack; Stack := n;

end;

// извлечение последнего добавленного элемента function Pop(var Stack: pNode): pointer;

var

n: pNode; begin

if Stack <> nil then begin

154

n := Stack; Result := n.data;

Stack := Stack.next; Dispose(n);

end else

Result := nil;

end;

// определение длины стека или очереди function Length(Stack: pNode): Boolean; var

p: pNode; i: integer;

begin

i := 0;

p := Stack;

while p<>NIL do begin i := i+1;

p := p^.next; end;

Result := i; end;

// удаление всех элементов из стека procedure Clear(var Stack: pNode); begin

while Stack <> NIL do begin data := Pop(Stack); dispose(data);

end; end;

Схожей со стеком структурой данных является очередь – упорядоченный набор некоторого переменного числа объектов, обработка которых ведется по правилу: «Первым пришел, первым вышел» (от англ. FIFO – First In, First Out), т.е. в порядке поступления данных. Например, часто используются очереди событий, очереди сообщений.

Для организации очереди в динамической памяти требуется, чтобы процедура PushQ и функция PopQ работали с противоположными элементами списка. Например, если процедура PushQ добавляет элемент в начало списка, то извлекать следует последний элемент списка, и наоборот. Пусть процедура PushQ добавляет элемент в конец списка. Тогда функция PopQ для очереди будет

155

идентична функции Pop для стека. Реализация функции PushQ может выглядеть так:

procedure PushQ(var Queue: pNode; data: pointer); var

n, p: pNode; begin

New(n);

n^.data := data; n^.next := NIL;

if Queue = NIL then Queue := n else begin

p := Queue;

while p^.next <> nil do p := p.next; p^.next := n;

end; end;

Для очереди имеет смысл хранить два указателя: на голову и хвост. Тогда для добавления элемента в очередь не нужно будет проходить по всем элементам до её конца, а сразу узнавать, «кто последний». Процедура PushQ, очевидно, станет на степень более эффективной.

Двунаправленные списки, бинарные деревья

Рассмотрим следующую структуру данных:

type

pNode = ^Node; Node = record

data: pointer; left: pNode; right: pNode;

end;

Два поля типа pNode дают возможность создания двусвязных динамических структур. Интерес представляет двунаправленный список, каждый элемент которого указывает как на следующий, так и предыдущий элемент списка. Двунаправленный список удобен для реализации очереди с двумя «хвостами» – дека (от англ. DEQ – double-ended queue). Наличие двусторонних связей обеспечивает высокую скорость доступа к элементам дека, а также свободное перемещение по деку в обе стороны. Для дека удобно использовать

156

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

Пример дека приведен на рис. 13.

Head

Data 1

 

#

 

 

 

 

Data 2

Data 3

Tail

Data N

#

 

 

 

 

 

Рис. 13. Схема организации дека в памяти

Помимо двунаправленных линейных списков эта же структура данных дает возможность построения бинарных деревьев. Первый элемент структуры – корень дерева (Root), при этом каждый элемент может иметь до двух дочерних элементов. Пример бинарного дерева представлен на рис. 14, знак # – синоним NIL.

Root

Data 1

 

Data 3

#

Data 2

 

Data 6 #

#

Data 5 #

#

Data 4 #

#

Рис. 14. Схема организации дерева в памяти

157

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

Сетевые структуры: графы

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

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

На рис. 15 представлен ориентированный граф.

1

2

3

5

4

Рис. 15. Пример ориентированного графа

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

158

1

 

 

 

 

 

 

 

#

 

 

 

 

 

 

 

 

 

2

#

3

#

4 #

5

#

 

 

 

#

 

 

 

 

 

 

Рис. 16. Схема организации графа в памяти

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

type

pArcElem = ^ArcElem; pNodeElem = ^NodeElem;

ArcElem = record Arc: pNodeElem; Data: pointer; Next: pArcElem;

end;

NodeElem = record Data: pointer; Arcs: pArcElem; Next: pNodeElem;

end;

Запись NodeElem способна хранить адрес полезной структуры данных, указатели на список исходящих связей типа ArcElem и следующий в списке узел типа NodeElem. В свою очередь запись

159

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

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

Сортировка с использованием линейных списков

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

Скелет программы сортировки может выглядеть следующим образом:

const

n = 100; type

tarray = array [1..n] of integer;

pNode = ^Node; Node = record

elem: integer; next: pNode;

end;

// процедура вставки с сохранение порядка

procedure OrderedInsert(var List: pNode; elem: integer); begin

end;

// основная процедура сортировки открытого массива procedure Sort(var a: array of integer);

var

160

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]