12. динамические структуры данных

advertisement
12. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ
Статическими называются такие данные, которые не меняют свои
размеры в течение всего времени своего существования. Регулярный и
комбинированный типы языка Pascal – это пример статических данных. Мы
всегда можем определить размер статических данных, взглянув на описание
данных, приведенное в программе.
В противоположность статическим данные динамической структуры
меняют свои размеры при выполнении программы. Чтобы понять, где могут
оказаться полезными данные динамической структуры, обсудим вопросы,
связанные с обработкой списков. Каждая компонента списка может быть
представлена например, переменной типа объект. Тип объект может быть
простым типом, например символьным или вещественным, или сложным
типом: регулярным или комбинированным. Можно представить список в
виде массива:
список: Array[1..размер_списка] of объект;
Но такое представление ставит несколько проблем:
1. Необходимо определить количество компонент списка, реально
существующих в момент начала работы. Это необходимо для того,
чтобы указать в описании значение размеров списка.
2. Добавлять новые компоненты можно только в конец списка.
Исключать компоненты неудобно из-за того, что в массиве остаются
дыры, которые надо каким-то образом отмечать.
3. Очень трудно соблюдать порядок среди компонентов списка, если мы
только не пойдеим на то, чтобы сортировать массив после добавления
каждой новой компоненты.
Существует красивое и эффективное решение этих проблем, связанное
с применением данных такой структуры, которая позволяет добавлять и
исключать компоненты, не заботясь о том, куда поместить новую
компоненту или что происходит со свободным пространством, возникающим
после исключения ненужных компонент. Для создания данных такой
структры будем пользоваться понятием ссылки.
12.1 Ссылки
Ссылочный тип – такой же простой тип, какими являются целый,
вещественный и булевский типы. Но для этого типа не зарезервировано в
языке никакого специального имени. Определение ссылочного типа
выглядит так:
Type
связь = ^объект;
Оно читается так: «тип связь есть ссылка на объект». Символ «^»
(стрелка) означает то, что связь является ссылочным типом. Переменная типа
объект может быть связана с некоторой ссылкой типа связь. Схематически
такая ситуация изображается так:
Переменная L на этой схеме имеет тип связь, а тот
L
L^
объект, который связан с ней , обозначается как L^.
объект
Распределение в памяти при этом выглядит так:
Адрес ячейки
Содержимое ячейки То есть, в какой-то ячейке с адресом
N,
выделенной
операционной
…
…
системой для хранения значения
N
L
переменной L типа связь, хранится
…
…
адрес ячейки памяти, где будет
L
^L
хранится значение типа объект.
…
…
Иногда нужны ссылки, с которыми не связана ни одна из переменных
типа объект. В этом случае будем писать:
L := NIL,
где NIL – служебное слово, обозначающее константу ссылочного типа,
которая опеределяет пустую ссылку, т.е. ссылку, которая "никуда не
указывает". Схема, соответствующая этому случаю:
L
Важно понимать разницу между ссылкой и тем объектом,на который
она ссылается. На следующем рисунке
P
показаны две переменные P и Q типа
связь,
ссылающиеся
на
различные
Q
объекты:
В результате выполнения присваивания P:=Q произойдет следующее:
Значение ссылки Q будет присвоено ссылке P. Обе ссылки теперь ссылаются на
квадрат,
треугольник
оказалмя
потерянным.
С другой стороны, после присваивания
следующее:
P
Q
P^ := Q^ произойдет
P
Теперь мы копируем значение объекта Q^ в
объект P^. Ссылки не изменились, но
изменилось значение объекта P^.
Q
Ссылка иногда называется указателем, поскольку она указывает на
объект, а не представляет его. Операция ^ часто называется в литературе
операцией разименования. На предыдущем рисунке указателем является P, а
результатом разименования P будет P^, т.е. треугольник.
Для определения данных динамической структуры нужно задать
объект, в состав которого будет входить ссылка. Кандидатами могут быть
объекты трех следующих типов: ссылочного, массива и записи. Ссылочный
тип можно исключить сразу, поскольку объекты этого типа не могут
содержать никакой информации, кроме значения самой ссылки. Компоненты
массива должны быть одного типа, что представляет собой довольно
значительное ограничение. Поэтому остановимся на записях. Мы можем
определить, например, следующий объект, содержащий кроме ссылки еще
некоторую информацию:
Type
связь = ^ объект;
объект = Record
следующий: связь;
данные: тип_данных
end;
Здесь возникает проблема «курицы и яйца». Что надо определять в
первую очередь: связь или объект? Разработчик Паскаля Н.Вирт предвидел
эту проблему, и поэтому разрешено определять ссылки на объекты перед
определением самих объектов.
Обратим внимание еще на одну особенность определения ссылочных
типов. Ссылка жестко связана с переменными того типа, для которого она
была определена. Например:
Type
P = ^A;
Q = B^;
Var
Pv : P;
Qv : Q;
Имея такие описания, запрещается делать присваивания:
Pv:=Qv и Qv:=Pv.
12.2. Связанные списки
Связанный список является простейшим типом даных динамической
структурпы. Компоненты связанного списка можно втавлять и искдлючать
произвольным образом. Схематически список можно изобразить так:
начало
‘Z’
‘Y’
‘X’
Здесь изобрахена структура данных, построенная на основе одиночной
ссылочной переменной «начало» и трех комплнент типа «объект». Данные
такой структры и называются связанным спичком.
Теперь рассмотрим, как можно строить связанные списию
Предположим, что ссылочная переменная «начало» принимается в ксчечстве
исходной точки для построения списка, как это изображено на рисунке.
Сначала список должен быть пуст. Поэтому запишем:
начало:=Nil.
Этому соответствует следущщая схема:
начало
Теперь надо вставить в список одну компоненту. Компоненты
создаются динамически с помощью процедуры New, аргументом которой
является ссылка. Пусть у нас есть описпние:
Var: p: связь.
Выполним процедуру:
New(p)
начало
p
То есть мы создали пока пустую компоненту с именем p^. Следующим
шагом будет присваивание новой компоненте некоторого значения. Так как
p^ - это запись, то обращение к полю «данные» будет выглядеть так:
p^.данные. Предполагая, что в поле «данные» можно поместить одиночный
символ, запишем:
p^.данные:=’X’
начало
p
‘X’
На следующем шаге заполняем поле «следующий»:
p^.следующий:=начало
начало
p
‘X’
До этого у переменной «начало» было значение Nil, поэтому в
результате выполнения последнего оператора присваивания значением поля
p^.следующий будет тоже Nil.
Теперь необходимо выполнить оператор:
начало:=p
начало
p
‘X’
Таким образом, построен список, состоящий из одной компоненты.
Добавим в список еще одну компоненту:
New(p);
начало
p
‘X’
p^.данные:=’Y’;
начало
‘X’
p
‘Y’
p^.следующий:=начало
начало
‘X’
p
‘Y’
начало:=p
начало
‘X’
p
‘Y’
Получен список, состоящий из двух компонент. При этом новая
компонента Y вставилась в начало списка.
12.3. Просмотр связанного списка
Доступ к первой компоненте списка осуществить легко, т.к. ее имя есть
«начало». Поэтому начало^.данные = ‘Z’. Вторая компонента списка
становится доступной благодаря тому, что в первой компоненте есть ссылка
с именем «следующий», т.е. именем второй компоненты будет
начало^.следующий^. Поэтому
начало^.следующий^.данные = ‘Y’.
Можно продолжить этот процесс, но для доступа к компонентам
длинного списка такой метод совершенно не подходит. Нужен алгоритм
динамического доступа. Этот алгоритм основан на том, что если p ссылается
на некоторую компоненту списка, то после выполнения присваивания:
p:=p^.следующий
p будет ссылатьься на компоненту, следующую за данной. Выполнение
этого оператора можно выполнять дро тех пор, пока значением p не станет
nil, т.е. пока мы не достигнем конца списка. В соответствии с этим, алгоритм
просмотра списка будет выглядеть так:
p:= начало;
while p<>nil do p:=p^.следующий;
Процесс последовательного обращения к компонентам списка
называется просмотром списка.
Следующая программа читает последовательность символов, строит из
них список, а затем печатает их в обратном порядке.
Program переупорядочитьсписок;
Type связь=^объект;
oбъект=record
следующий: связь;
данные: char
end;
var начало, p: связь;
c: char;
begin
read(c);
начало:=nil;
while c<>’.’ do
begin
new(p);
p^.данные:=c;
p^.следующий:=начало;
начало:=p;
read(c)
end;
{просмотр списка}
p:=начало;
while p<>nil do
begin
write(p^.данные);
p:=p^.следующий
end
end.
На примере видно, что простой свсязанный список соответствут так
называемой структуре «последний пришел, первый вышел» или LIFO (Last
Input First Output) Структура с такимим свойствами называется стеком.
12.3. Очереди
Примененеие простого свсязанного списка ограничичвается из-за
недостаточно удобного доступа ко всем компонентам спсика, кроме первой.
Не трудно реорганизовать список так, чтобы превратить его в очередь.
Очередь – структура. у которой доступна лишь компонента.
находящаяся в этой очереди наибольшее время. Такие структуры называются
FIFO (первым пришел – первым вышел). Такая структура представляет собой
связанный список, но требуется еще одна ссылка на конец очереди.
var
связь = ^объект;
объект =record
следующий: связь;
данные: типданных;
end;
головной, замыкающий: связь;
Головной
Замыкающий
Следующая процедура исключаеь их очереди первую компоненту и
настраиваеь на нее ссылку "первый". Если при этом исключается последняя
компонента очереди и очередь становится пустой, то предпринимаются
некоторые специальные действия.
procedure убратьизочереди (var первый, головной, замыкающий: связь);
begin
первый:= головной;
if головной<>nil then
begin
головной:=головной^.следующий;
if головной=nil then замыкающий := nil
end
end;
Если при вызове процедуры "убратьизочереди" оказывается, что
очередь пуста, будет возвращено значение nil, указывающее, что убирать из
очереди нечего.
Если очередь становится пустой в результате исключения из нее
последней компоненты, значение nil получают обе переменные – и
"головной", и "замыкающий".
Процедура "вставитьвочередь" помещает в конец очереди новую
компоненту. Ссылка на новую компоненту содержится в переменной
"новый", причем устанавливается эта ссылка в вызывающей программе.
procedure вставитьвочередь (новый: связь; var головной, замыкающий: связь);
begin
if головной = nil then головной := новый
else замыкающий^.следующий := новый;
замыкающий := новый
end;
Представим графически случай непустой очереди:
Головной
Замыкающий
Новый
Из текста процедуры видно, что в случае работы с пустой очередью
также выполняются некоторые специальные действия:
Головной
Замыкающий
Новый
Download