Алгоритмические языки высокого уровня. Курс лекций
.pdfСтатические структуры данных относятся к разряду неприми-
тивных структур, которые представляют собой структурированное множество примитивных, базовых, структур. Например, вектор мо- жет быть представлен упорядоченным множеством чисел. Поскольку по определению статические структуры отличаются отсутствием из- менчивости, память для них выделяется один раз и ее объем остается неизменным до уничтожения структуры. Слово «статический» от- носится скорее к реализации структуры, нежели к абстрактному типу данных.
Простейшая статическая структура данных – массив, где обраще- ние к элементу происходит через его номер. Слово «массив» употреб- ляется в различных контекстах: как абстрактный тип данных (т.е. множество с операциями: получить элемент с номером N, записать элемент с номером N) или как физическая структура, реализованная в виде непрерывной области памяти (в случае реализации массива через такую структуру номер соответствует смещению элемента от начала области). Плюсов у массива (здесь реализация) всего два: доступ за конечное время к любому элементу; память тратится только на дан- ные. Минус один: статичность, неизменность структуры.
Динамические структуры данных по определению характери-
зуются отсутствием физической смежности элементов структуры в памяти, непостоянством и непредсказуемостью размера (числа эле- ментов) структуры в процессе ее обработки. Поскольку элементы динамической структуры располагаются по непредсказуемым адре- сам памяти, адрес элемента такой структуры не может быть вычис- лен из адреса начального или предыдущего элемента. Для установ- ления связи между элементами динамической структуры использу- ются указатели, через которые устанавливаются явные связи между элементами. Такое представление данных в памяти называется связ- ным.
Элемент динамической структуры состоит из двух полей:
•информационного поля (полей), в котором содержатся данные (в общем случае информационное поле само может являться слож- ной интегрированной структурой – вектором, массивом, другой ди- намической структурой и т.п.);
•поля ссылок (связок), в котором содержатся один или несколько указателей, связывающих данный элемент с другими элементами структуры.
Когда связное представление данных используется для решения прикладной задачи, для конечного пользователя «видимым» делается
101
только содержимое информационного поля, а поле ссылок использу- ется только программистом-разработчиком.
Достоинства связного представления данных:
•возможность обеспечения изменчивости структур;
•размер структуры ограничивается только доступным объемом памяти ПК;
•при изменении логической последовательности элементов структуры требуется не перемещение данных в памяти, а только кор- рекция указателей;
•большая гибкость структуры.
Вместе с тем связное представление имеет и недостатки, основ- ные из которых:
•на поля связок расходуется дополнительная память;
•доступ к элементам связной структуры может быть менее эф- фективным по времени.
Последний недостаток является наиболее серьезным, и именно им ограничивается применимость связного представления данных. Если в смежном представлении данных для вычисления адреса любого эле- мента во всех случаях достаточно номера элемента и информации, со- держащейся в дескрипторе структуры, то для связного представления адрес элемента не может быть вычислен из исходных данных. Деск- риптор связной структуры содержит один или несколько указателей, позволяющих войти в структуру, далее поиск требуемого элемента выполняется следованием по цепочке указателей от элемента к эле- менту. Поэтому связное представление практически никогда не при- меняется в задачах, где логическая структура данных имеет вид векто- ра или массива – с доступом по номеру элемента, но часто применяет- ся в задачах, где логическая структура требует другой исходной ин- формации доступа (таблицы, списки, деревья и т.д.). Динамические структуры данных служат полезным дополнением к стандартным структурам, уже определенным в языке Pascal. Динамическими они называются потому, что их элементы создаются и уничтожаются «на ходу» – в процессе работы программы. Для любой динамической структуры данных должны быть определены операции для работы со структурой данных: добавление, удаление, поиск и др.
Списком называется упорядоченное множество, состоящее из пе- ременного числа элементов, к которым применимы операции вклю- чения, исключения. Список, отражающий отношения соседства меж- ду элементами, называется линейным. Длина списка равна числу
102
элементов, содержащихся в списке, список нулевой длины называет- ся пустым списком. Линейные связные списки являются простей- шими динамическими структурами данных (графически связи в спи- сках удобно изображать с помощью стрелок. Если компонента не связана ни с какой другой, то в поле указателя записывают значение, не указывающее ни на какой элемент. Такая ссылка обозначается специальным именем – nil).
Структура односвязного списка. Поле INF – информацион-
ное поле, данные, NEXT – указатель на следующий элемент списка.
Каждый список должен иметь особый элемент, называемый ука- зателем начала списка или головой списка, который обычно по фор- мату отличен от остальных элементов. В поле указателя последнего элемента списка находится специальный признак nil, свидетельст- вующий о конце списка.
Двусвязный список характеризуется наличием пары указателей в каждом элементе: на предыдущий элемент и на следующий. Плюс тут в том, что от данного элемента структуры можно пойти в обе сто- роны. Таким образом, упрощаются многие операции. Однако на указатели тратится дополнительная память.
Разновидностью рассмотренных видов линейных списков являет- ся кольцевой список, который может быть организован на основе как односвязного, так и двухсвязного списков. При этом в односвязном списке указатель последнего элемента должен указывать на первый элемент; в двухсвязном списке в первом и последнем элементах со- ответствующие указатели переопределяются.
103
При работе с такими списками несколько упрощаются некоторые процедуры. Однако при просмотре такого списка следует принять неко- торые меры предосторожности, чтобы не попасть в бесконечный цикл.
Стеком называется динамическая структура данных, у которой в каждый момент времени доступен только верхний (последний) эле- мент.
Понятие стека можно проиллюстрировать примером стопки книг: в стопку можно добавить еще одну книжку, положив ее сверху; из стопки можно взять, не разрушив ее, только верхнюю книгу. А для того чтобы достать книжку из середины стопки, необходимо сначала убрать по одной все лежащие выше нее. Последовательность обра- ботки элементов стека хорошо отражают аббревиатуры LIFO (Last In First Out – «последним вошел, первым вышел») и FILO (First In Last Out – «первым вошел, последним вышел»).
Для стека должны быть определены следующие операции:
empty(<нач_стека>):boolean |
– проверка стека |
|
на пустоту; |
add(<нач_стека>,<новый_элемент>):<нач_стека> – добавление эле-
мента в стек;
Take(<нач_стека>):<тип_элементов_стека> |
– получение зна- |
|
чения верхнего |
|
элемента; |
del(<нач_стека>):<нач_стека> |
– удаление верх- |
|
него элемента из |
|
стека. |
104
Очередью называется динамическая структура, у которой в каж- дый момент времени доступны два элемента: первый и последний. Из начала очереди элементы можно удалять, а к концу – добавлять.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
D |
|
|
|
|
|
|
|
|
C |
|
C |
|
|
D |
|
|
|
|
B |
|
B |
|
B |
|
|
C |
|
D |
A |
|
A |
|
A |
|
A |
|
|
B |
|
C |
|
|
|
|
|
|
|
|
|
|
|
|
Примером очереди может служить, например, очередь в продук- товом магазине. Последовательность обработки элементов очереди хорошо отражают аббревиатуры LILO (Last In Last Out – «послед- ним вошел, последним вышел») и FIFO (First In First Out – «первым вошел, первым вышел»).
Для очереди должны быть определены следующие операции:
empty(<нач_очереди>):boolean |
– проверка очереди на пустоту; |
||
add(<кон_очереди>,<нов_эл- |
– добавление элемента в конец очере- |
||
т>):<кон_очереди> |
ди; |
|
|
Take_beg(<нач_очереди>):<тип_эл- – получение |
значения |
первого эле- |
|
тов_очереди> |
мента; |
|
|
Take_end(<кон_очереди>):<тип_эл- – получение |
значения |
последнего |
|
тов_очереди> |
элемента; |
|
|
del(<нач_очереди>):<нач_очереди> – удаление элемента из начала очере-
ди.
Дек – особый вид усложненной очереди (дек – deque – DoubleEnded QUEue – двухконцевая очередь). В каждый момент времени у дека доступны как первый, так и последний элементы, причем до- бавлять и удалять элементы можно и в начале, и в конце дека. Таким образом, дек – это симметричная двусторонняя очередь. Частный случай дека – дек с ограниченным входом и дек с ограниченным вы- ходом. Набор операций для дека аналогичен набору операций для очереди, с той лишь разницей, что добавление и удаление элементов можно производить и в конце, и в начале структуры (задачи, для ре- шения которых требуется использование структуры дека, встречают- ся в вычислительной технике и программировании гораздо реже, чем задачи, реализуемые на структуре стека или очереди). Примером де- ка может служить некий терминал, в который вводятся команды, ка- ждая из которых выполняется какое-то время. Если ввести следую- щую команду, не дождавшись, пока закончится выполнение преды- дущей, то она встанет в очередь и начнет выполняться, как только
105
освободится терминал. Это FIFO очередь. Если же дополнительно ввести операцию отмены последней введенной команды, то получа- ется дек).
Бинарное дерево – это иерархическая динамическая структура данных, состоящая из узлов и соединяющих их дуг. В каждый узел, кроме одного, ведет ровно одна дуга. Этот единственный узел назы- вается корнем дерева.
С каждой вершиной дерева связывается конечное число отдель- ных деревьев, называемых поддеревьями. Вершина Y, находящаяся непосредственно ниже вершины X, называется непосредственным потомком X, а вершина X называется предком Y. Если вершина не имеет потомков, то она называется листом, если имеет, то называет-
ся внутренней вершиной.
Примеры различных списочных структур приведены на рис. 4.1:
a)односвязный (линейный) список: структура, каждый элемент которой «знает» адрес только следующего за ним элемента (стек, очередь);
b)двусвязный линейный список: структура, каждый элемент которой «помнит» адрес не только следующего, но и предыдущего элемента списка (дек);
c)бинарное дерево может быть представлено двусвязным нели- нейным списком.
Каждая вершина помнит обоих своих возможных потомков. Если каждой вершине необходимо помнить не только потомков, но и предка, то список становится трехсвязным;
d)для представления ориентированного графа используются ие- рархические списки – комбинация из двух линейных списков: вер- шины задаются структурой, содержащей три поля, а дуги – структу-
106
рой, содержащей два поля (показан ориентированный граф, пред- ставленный приведенной списочной структурой).
Рис. 4.1. Примеры списочных структур
Описание списочных структур. Каждый элемент односвязного списка хранит адрес другого элемента из этого же списка.
Описание этой структуры:
type ukazatel = ^element_spiska; element_spiska = record znachenie : integer;
next : ukazatel;
end;
Замечание: кажется, что гораздо более естественным было бы отнести поле next_element к типу pointer. Однако не- удобства, которые возникнут из-за нетипизированности указате- лей в процессе написания программы, будут гораздо серьезнее, чем одна лишняя строчка при описании типов.
Описание списочных структур
a) односвязный список |
type ukazatel = ^elem_spiska; |
|
elem_spiska = record |
|
znach : integer; |
107
b)двусвязный
список
next : ukazatel; end;
линейный type point = ^element_spiska; list = record znachenie : in-
teger;
sled : point pred : point;
end;
с) бинарное дерево хический список)
(иерар- type point = ^tree; tree = record
data : integer; left_sibling : point; right_sibling: point;
end;
d)ориентированный граф (двусвязный нелинейный список)
type uk_versh = ^versh; uk_duga = ^duga;
vershina = record nomer : integer;
sled_versh : uk_versh; spisok_dug : uk_duga; end;
duga = record konec_dugi : uk_versh; sled_duga : uk_duga; end;
Оперирование элементами списка
Хранение списка. Для того чтобы хранить информацию обо всем списке, достаточно только одной (нескольких) переменной – указа- теля на первый элемент (последний и т.п.) этого списка. Обычно его называют головой (началом) списка. Указатель на голову должен быть выделенным: с ним нельзя производить никаких действий, ко- торые могут стать причиной утери всего списка.
Для работы со списком обычно заводят вспомогательный указа- тель (несколько указателей) var head,p,q: ukazatel;
Обращение к элементам списка. Если есть указатель, указы-
вающий на некоторый элемент списка, то содержимое полей этого элемента и даже следующих за ним можно получить так:
P |
адрес текущего элемента списка; |
p^ |
запись из нескольких полей, хранящаяся по |
|
адресу p; |
p^.znachenie |
значение первого поля этой записи; |
p^.next_element |
значение второго поля этой записи, являю- |
|
щееся адресом следующего элемента списка; |
108
p^.next_element^.znachenie значение, хранящееся в первом поле элемента списка, следующего за тем, на который ука- зывает р.
Создание списка. Чтобы использовать список, надо определить тип компонентов списка и переменную – указатель на первый эле- мент списка. После создания необходимо поместить объект в список. Добавлять элементы можно в начало, конец или в нужное место спи- ска. Во всех этих случаях необходимо корректировать указатели. Чтобы добавлять элемент в упорядоченный список, нужно найти узел, за которым будет следовать добавляемый элемент, и устано- вить его указатель на новый элемент, а указатель нового узла устано- вить на узел, перед которым он вставляется.
Var Head, x : ukazatel;
Создание первого элемента:
New(Head); { выделяем место в памяти для перемен-
ной Head }
Head^.Next:=nil; { указатель на следующий эле- мент пуст (такого элемента нет) }
Head^.Znach:=3; { заполняем информационное поле первого элемента }
Создание следующего элемента – нужно добавить элемент в ко- нец списка.
109
Введем вспомогательную переменную указательного типа, кото- рая будет хранить адрес последнего элемента списка:
x:=Head; {сейчас последний элемент списка сов- падает с его началом}
New(x^. Next); {выделим области памяти для сле- дующего (2-го) элемента и поместим его адрес в адресную часть предыдущего (1-го) элемента }
x := x^. Next; {переменная x принимает значение адреса выделенной области, таким образом осущест- вляется переход к следующему (2-му) элементу спи- ска }
x^. Znach:=5; { значение этого элемента } x^. Next:=nil; {следующего значения нет }
Остальные элементы списка создаются аналогично:
New(x^.Next); {выделим области памяти для сле- дующего элемента}
x := x^.Next; { переход к следующему (3-му) элементу списка }
x^. Znach:=1; { значение этого элемента } x^.Next:=nil; {следующего значения нет } New(x^.Next); { выделим области памяти для сле-
дующего элемента }
x := x^.Next; { переход к следующему (4-му) элементу списка }
x^. Znach:=9; { значение этого элемента }
110
