Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
delphi / песни о паскале.pdf
Скачиваний:
63
Добавлен:
26.03.2016
Размер:
5.16 Mб
Скачать

Глава 54 Односвязные списки

Создав массив указателей на динамические переменные, мы сбросили в кучу уйму данных. Результат неплох, но где пределы совершенству? Как ни крути, размер массива ограничен, сколько указателей нам запасти? Побольше? Но тогда значительная часть массива будет пустовать. Одним словом, постоянный размер массива вяжет нас. Идеальное решение в том, чтобы отвести под данные столько памяти, сколько им требуется, — не больше и не меньше. Мы стремимся к этому решению и сейчас в шаге от цели.

Чудесное сочетание

Ключ к безупречному решению — в сочетании записи с указателем. Запись может содержать в своей обёртке разнородные элементы: числа, символы, строки, массивы и так далее. А если внедрить в неё указатель на другую запись этого же типа? Тогда мы сможем погрузить в кучу цепочку элементов так, как показано на рис. 120.

Указатель на первую запись Статическая Адрес (голова списка)

память

программы

Куча

Данные

Данные

Адрес

NIL

Данные

Адрес

Рис. 120 – Связывание записей указателями

Здесь в кучу погружены три элемента данных (три записи), а в статической памяти программы торчит лишь «поплавок» — указатель на первый элемент этой цепочки. Все последующие записи сцеплены друг с другом указателями, встроенными в них же. Последняя запись содержит NIL, — как говорится, сколько веревочке не виться… Наряду с указателями, записи содержат, разумеется, и данные, необходимые для решаемой задачи. Такую структуру называют односвязным списком. Односвязным, — потому что элементы сцеплены лишь одной связью (при желании таких связей можно сделать больше). Мы будем называть эту динамическую структуру просто списком, по-английски — List.

421

Глава 54

Односвязные списки

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

Проблема курицы и яйца

Построим элемент односвязного списка для полицейской базы данных. Вот объявления нужных нам типов данных: записи и указателя на неё.

type

PRec = ^TRec;

{ Указатель на запись, – здесь TRec ещё не объявлен! }

TRec = record

 

{ Тип записи для базы данных }

mNumber

: integer;

{ Номер авто }

mFam

: string[31];

{ Фамилия владельца }

mNext

: PRec;

{ Указатель на следующую запись }

end;

 

 

Тип TRec содержит, наряду с полезными полями mNumber и mFam, ещё одно служебное поле — указатель на следующую запись mNext (Next — «следующий»).

Друзья мои, обратите внимание на подчеркнутую строку, где нарушено важнейшее правило Паскаля! Вы знаете, что любой объект программы объявляют до его применения. Внутри записи объявлен указатель на неё же — поле mNext. Поэтому тип этого указателя PRec надо объявить раньше типа записи TRec, что и сделано в первой строке. Однако в этой первой строке применён тип записи TRec, который объявлен ниже! Круг замкнулся, как в задаче о курице и яйце, — что появилось раньше?

К счастью, в Паскале эта задача решена: указатель на любой тип данных можно объявлять раньше того типа, на который он ссылается. Это единственное исключение из правила. Значит, представленное выше объявление типов вполне законно.

Вяжем список

Возьмемся за постройку списка для полицейской БД. Начнем с простого: вставим в список несколько элементов данных с номерами автомобилей и фамилиями владельцев, а затем распечатаем список. Это делает программа P_54_1, рассмотрим её.

422

Глава 54

Односвязные списки

{ P_54_1 – Размещение данных в несортированном списке }

type

PRec = ^TRec;

{ Тип указатель на запись }

 

TRec = record

 

{ Тип записи для базы данных }

 

mNumber

: integer;

{ Номер авто }

 

mFam

: string[31];

{ Фамилия владельца }

 

mNext

: PRec;

{ Указатель на следующую запись }

 

end;

 

 

var

List : PRec;

{ Указатель на начало списка (голова) }

{ Добавление в список записи об автомобиле } procedure AddToList(aNumber: integer; const aFam : string); var P : PRec;

begin

New(P); { Создаем динамическую переменную-запись } { Размещаем данные в полях записи }

P^.mNumber:= aNumber; P^.mFam:= aFam;

P^.mNext:= List; { Цепляем предыдущую запись к новой записи } List:= P; { Теперь голова указывает на новую запись }

end;

{ Распечатка списка } procedure PrintList;

var P : PRec; begin

P:= List;

while Assigned(P) do begin Writeln(P^.mNumber, '':3, P^.mFam); P:= P^.mNext;

end;

end;

begin { Главная программа } List:= nil;

AddToList(10, 'Иванов');

AddToList(20, 'Петров');

AddToList(30, 'Сидоров');

PrintList;

Readln;

end.

Основу программы составляют процедуры вставки в список и его распечатки. В процедуре AddToList — добавить в список — первые строки вам знакомы: после создания динамической переменной P^ данные копируются в её поля.

423

Глава 54

Односвязные списки

Обратите внимание на то, что указатель P — это локальная переменная, которая исчезнет после выхода из процедуры. И тогда, если не сохранить этот указатель, адрес новой переменной будет утерян, что приведет к утечке памяти. Поэтому после создания новой переменной P^ адрес из головы списка List копируется в поле mNext вновь созданной записи, и адрес новой записи P помещается в голову списка. Вот эти операторы.

P^.mNext:= List;

{

Цепляем предыдущую запись к новой записи }

List:= P;

{

Теперь голова указывает на новую запись }

 

 

 

Всё просто, но если эти строки поменять местами, катастрофа неминуема!

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

На рис. 121 и рис. 122 выполняется вставка первого элемента. В начальный момент, когда список ещё пуст, его голова — глобальная переменная List — содержит заглушку NIL, помещенную туда главной программой.

В результате присваивания оператором

P^.mNext:= List;

значение NIL попадает в поле новой переменной P^.mNext (после создания переменной это поле было не определено и содержало мусор).

List –

 

 

голова

2

P

списка

 

 

 

NIL

Адрес_1

1

Данные_1

???

Рис. 121 – Состояние списка перед вставкой первого элемента

Следующий затем оператор

424

Глава 54

Односвязные списки

List:= P;

сохраняет указатель на вновь созданную переменную в голове списка List. Теперь, перед выходом из процедуры, на эту переменную ссылаются уже два указателя (рис. 122). Но поскольку P — это локальная переменная, которая вскоре исчезнет, то после выхода из процедуры единственной ссылкой на первый элемент останется голова списка List.

List

P

Адрес_1

Адрес_1

Данные_1

NIL

Рис. 122 – Состояние списка после вставки первого элемента

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

List

2

P

Адрес_1

Адрес_2

 

1

Данные_1

NIL

Данные_2

???

Рис. 123 – Состояние списка перед вставкой второго элемента

425

Соседние файлы в папке delphi