Лабораторная работа №1. Указатели и связные списки

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

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

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

В Object Pascal указатели используются в любом типе данных, требующем динамического выделения больших блоков памяти. Например, длинные строки и объекты классов являются неявными указателями. Указатели всегда неявно присутствуют как бы «за кадром» исходного кода, даже если явно их в коде нет. Понимание указателей и операций с указателями необходимо для работы с Object Pascal. Более того: во многих приемах программирования указатели используются явно.

Использование указателей позволяет экономнее расходовать память, потому что с их помощью память для переменных можно выделять динамически. Однако динамическое выделение памяти в то же время является и недостатком использования указателей. Программист должен постоянно помнить о необходимости удаления неиспользуемых динамических объектов, иначе в программе произойдет утечка памяти. Интенсивная утечка памяти может вызвать крах программы, причем обнаружить источник утечки довольно трудно.

НЕ нашли? Не то? Что вы ищете?

В Object Pascal символ «крышечка» (^) используется как для обозначения указателя, так и для его разадресации. Синтаксис определения типа указателя имеет вид

type

имя_типа_указателя = ^тип;

Например, оператор

type

IntPointer = ^Integer;

определяет тип указателя IntPointer. Переменная этого типа является указателем на целое значение, т. е. переменная типа IntPoiner содержит адрес ячейки памяти, в которой хранится значение типа Integer.

Если определен тип указателя, то можно объявить указатель этого типа. Например, оператор

intptr:IntPointer;

объявляет указатель intPtr типа IntPointer.

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

имя_указателя^

разадресует указатель. Например, если переменная intptr имеет тип IntPointer, то выражение intptr^ возвращает целое значение.

В Object Pascal есть зарезервированное ключевое слово nil, обозначающее специальную константу, значение которой можно присвоить любой указательной переменной. Если указателю присвоено значение nil, то это означает, что указатель не ссылается ни на какой объект. Поэтому рекомендуется всегда инициализировать указатели значением nil.

Для создания и уничтожения динамических переменных в Object Pascal используются встроенные процедуры New() и Dispose(). Процедура New() выделяет память для новой динамической переменной и присваивает указателю значение адреса выделенной области памяти. Когда созданная процедурой New() динамическая переменная больше не нужна, ее следует удалить с помощью процедуры Dispose(), т. е. освободить память, выделенную для этой переменной. Синтаксис процедур New() и Dispose() имеет вид

New(имя_указателя);

Dispose(имя_указателя);

После вызова процедуры Dispose() значение указателя становится неопределенным.

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

type

IntPointer = ^IntRecord;

IntRecord = record

value: Integer;

next: IntPointer;

end;

var

top, temp: IntPointer;

count: Integer;

begin

New(top);

temp := top;

{Создание связного списка}

for count := 1 to 10 do begin

temp^.value := count;

if (count < 10) then begin

New(temp^.next);

temp := temp^.next;

end

else begin

temp^.next := nil;

end;

end;

{Вывод}

temp := top;

while (temp <> nil) do begin

Writeln(temp^.value);

temp := temp^.next;

if (temp <> nil) then begin

writeln('');

writeln('|');

writeln('V');

writeln('');

end;

end;

{Уничтожение}

while (top <> nil) do begin

temp := top;

top := top^.next;

Dispose(temp);

end;

readln;

end.

В следующем примере указатели используются для создания связного списка имен. Программа LinkedList содержит процедуры инициализации списка, добавления в список узлов, удаления узлов из списка, вывода содержимого списка на экран и уничтожения списка. Обратите внимание: цикл while в процедуре Remove() содержит булево выражение, требующее режима неполного вычисления, так как при list, равном nil, объекта list^.name не существует.

program LinkedList;

{$APPTYPE CONSOLE}

uses

SysUtils;

type

NameType = String[20];

ListPointer = ^ListType;

ListType = record

name: NameType;

next: ListPointer;

end;

{Инициализация связного списка}

procedure InitializeList(var top: ListPointer);

begin

top := nil;

end;

{Добавление в связный список узла, содержащего имя}

procedure Add(var top: ListPointer; name: NameType);

var

list: ListPointer;

begin

if (top = nil) then begin

New(top);

list := top;

end

else begin

list := top;

while (list^.next <> nil) do begin

list := list^.next;

end;

New(list^.next);

list := list^.next;

end;

list^.name := name;

list^.next := nil;

end;

{Удаления из связного списка узла, содержащего имя}

procedure Remove(var top: ListPointer; name: NameType);

var

list, prev: ListPointer;

begin

list := top;

prev := list;

while (list <> nil) and (list^.name <> name) do begin

prev := list;

list := list^.next;

end;

if (list <> nil) then begin

if (prev = list) then begin {Удаление первого узла}

prev := prev^.next;

top := prev;

end

else begin {Удаление второго, или}

prev^.next := list^.next; {последующео узла}

end;

Dispose(list);

end;

end;

{Вывод связного списка}

procedure DisplayList(top: ListPointer);

var

lineOut: String;

begin

lineOut := '';

while (top <> nil) do begin

lineOut := lineOut + top^.name;

top := top^.next;

if (top <> nil) then begin

lineOut := lineOut + ' --> ';

end;

end;

frmLinkedList. memOutput. Lines. Add(lineOut);

end;

{Уничтожение связного списка}

procedure DestroyList(var top: ListPointer);

var

temp: ListPointer;

begin

while (top <> nil) do begin

temp := top;

top := top^.next;

Dispose(temp);

end;

end;

var

list: ListPointer;

begin

InitializeList(list);

Add(list, 'Jack');

Add(list, 'Bill');

Add(list, 'Tom');

Add(list, 'Harry');

DisplayList(list);

Remove(list, 'Jack');

DisplayList(list);

Remove(list, 'Harry');

DisplayList(list);

Add(list, 'Sally');

DisplayList(list);

Remove(list, 'Tom');

DisplayList(list);

Add(list, 'Lucy');

DisplayList(list);

DestroyList(list);

readln;

end.

Операции с указателями и указатель pointer

Для указателей определены только операции присваивания и проверки на равенство и неравенство. В Object Pascal запрещается любые арифметические операции с указателями, их ввод-вывод и сравнение на больше-меньше.

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

Процедура GetMem(P,Size), где P – переменная-указатель, Size – размер выделяемой памяти в байтах, позволяет выделить в динамической памяти область размера Size, при этом адрес выделенной области присваивается переменной P.

Процедура FreeMem(P,Size), где P – переменная-указатель, Size – размер памяти в байтах, освобождает область в памяти, адрес которой задается указателем P, а размер равен Size

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

В Object Pascal определены стандартные функции для работы с указателями:

– add(x):pointer – возвращает адрес x (аналогично операции @).

– ptr(x):pointer – по заданному сегменту и смещению формирует адрес типа poiner.

– seg(x):word – возвращает адрес сегмента для x.

– ofs(x):word – возвращает смещение для x.

При работе с динамической памятью часто применяются вспомогательные функции:

– Maxavail:longint – возвращает длину в байтах самого длинного свободного участка динамической памяти.

– Memavail:longint – возвращает полный объем свободной динамической памяти в байтах.

– Sizeof(x):word – возвращает объем в байтах, занимаемый х.

Списки, стеки, очереди и очереди с двухсторонним доступом

Список – это упорядоченный набор данных. Первый узел списка называется называется головой, а последний – хвостом. Списки, стеки, очереди и очереди с двухсторонним доступом представляют собой специальные виды списков, добавление и удаление данных из которых регулируется определенными правилами. Реализация этих структур на основе статических массивов работает удовлетворительно только при относительно небольшом объеме данных. Динамические массивы более гибкие, с их помощью можно обрабатывать данные произвольного объема. Указатели позволяют создавать наиболее быстродействующие коды на основе оптимальных решений. Обычно указатели используются для обработки особенно больших объемов данных, однако при этом программисту приходится брать на себя дополнительную задачу управления памятью.

Наглядно стек можно представить себе как стопку тарелок, стоящих на столе. Когда рабочий в кафе моет посуду, он ставит каждую вымытую тарелку на самый верх стопки чистых тарелок. Когда в кафе заходит клиент, чистая тарелка для него снимается с верха стопки. Таким образом, последняя тарелка, поставленная в стопку, оказывается первой, извлеченной из стопки. Говорят, что стек работает по принципу LIFO (last-in first-out, т. е. последним зашел – первым вышел).

Процессы добавления и чтения данных из стека называются занесением в стек и из­влечением из стека (иногда используются термины «заталкивание в стек» и «выталкива­ние из стека»). Механизм работы стека напоминает магазин автомата, в который солдат заталкивает один патрон за другим. Во время стрельбы механизм автомата извлекает па­троны тоже один за другим. Когда очередной патрон извлечен, на его место немедленно подается новый.

В компьютерном программировании используются термины вершина и дно стека. Вершиной стека является элемент данных, подлежащий извлечению. Между магазином автомата и стеком есть существенное отличие. В магазине автомата двигаются патроны, а в стеке элементы данных остаются неподвижными (данные не переписываются из ячей­ки в ячейку), вместо этого передвигается указатель на вершину стека, т. е. изменяется значение указателя. С этой точки зрения стек больше похож все же на стопку тарелок. Продемонстрируем работу стека с помощью следующего примера. Код программы StackEx реализует стек целых чисел на основе статического массива. Указателем на вершину стека является переменная topOf Stack. Как видно из программы, при инициализации указа­телю стека присваивается значение nil, т. е. если значение указателя равно nil, значит, стек пуст.

Для занесения и извлечения данных из стека предназначены процедуры Pop() и Push(). Статический массив стека stack типа stackType и относящиеся к нему переменные имеют глобальную область видимости.

program StackEx;

{$APPTYPE CONSOLE}

uses

SysUtils;

const

STACKSIZE = 50; {Максимальный размер стека}

type

StackType = array[1..STACKSIZE] of Integer; {Тип стека целых чисел}

var

stack: StackType;

topOfStack: Integer;

function Rus(mes: string):string;

// В ANSI русские буквы кодируются числами от 192 до 255,

// в ASCII - от 128 до 175 (А..Я, а..п) и от 224 до 239 (р..я)

var

i: integer; // номер обрабатываемого символа

begin

for i:=1 to length(mes) do

case mes[i] of

'А'..'п' : mes[i] := Chr(Ord(mes[i]) - 64);

'р'..'я' : mes[i] := Chr(Ord(mes [i]) - 16);

end;

rus := mes;

end;

{Инициализация стека}

procedure Initialize;

begin

topOfStack := 0;

end;

{Функция IsEmpty возвращает True, если стек пуст}

function IsEmpty: Boolean;

begin

IsEmpty := (topOfStack = 0);

end;

{Функция IsFull возвращает True, если стек заполнен}

function IsFull: Boolean;

begin

IsFull := (topOfStack = STACKSIZE);

end;

{Извлечение целого числа с вершины стека}

procedure Pop(var value: Integer);

begin

if IsEmpty then begin

writeln(rus('Недопустимая операция извлечения -- стек пуст!'));

end

else begin

value := stack[topOfStack];

Dec(topOfStack);

end;

end;

{Занесение целого числа в стек}

procedure Push(value: Integer);

begin

if IsFull then begin

writeln(rus('Недопустимая операция занесения -- стек заполнен!'));

end

else begin

Inc(topOfStack);

stack[topOfStack] := value;

end;

end;

{Тестирование стека}

var

value, count: Integer;

begin

Initialize;

for count := 1 to 10 do begin

Push(count);

Writeln(rus('Занесено: ') + IntToStr(count));

end;

for count := 1 to 5 do begin

Pop(value);

Writeln(rus('Извлечено: ') + IntToStr(value));

end;

for count := 30 to 33 do begin

Push(count);

Writeln(rus('Занесено: ') + IntToStr(count));

end;

for count := 1 to 9 do begin

Pop(value);

Writeln(rus('Извлечено: ') + IntToStr(value));

end;

readln;

end.

Принцип действия очереди противоположен принципу действия стека. Стек похож на стопку тарелок, стоящую на столе, в то время как очередь напоминает живую очередь клиентов кафе. Первый клиент в очереди всегда обслуживается первым. Таким образом, очередь работает по принципу FIFO (fist-in fist-out, первым вошел – первым вышел). Когда данные добавляются в конец очереди, говорят, что они ставятся очередь. Термины «добавление» или «занесение в очередь» – синонимы термин «постановка в очередь». В программировании используются также термины начало и конец очереди. В начале очереди находится элемент данных, предназначенный для извлечения из очереди на следующем шаге.

В программе Queue реализована очередь целых чисел на основе динамического массива. Процедуры Enqueue() и Dequeue() используются для постановки чисел в очередь и извлечения из очереди. Используемая в модуле QueueEx методика реализации очереди широко применяется в реальных задачах компьютерного моделирования, требующих высокого быстродействия программы.

program Queue;

{$APPTYPE CONSOLE}

uses

SysUtils;

function Rus(mes: string):string;

// В ANSI русские буквы кодируются числами от 192 до 255,

// в ASCII - от 128 до 175 (А..Я, а..п) и от 224 до 239 (р..я)

var

i: integer; // номер обрабатываемого символа

begin

for i:=1 to length(mes) do

case mes[i] of

'А'..'п' : mes[i] := Chr(Ord(mes[i]) - 64);

'р'..'я' : mes[i] := Chr(Ord(mes [i]) - 16);

end;

rus := mes;

end;

type

QueueType = array of Integer; {Тип очереди целых чисел}

var

queue: QueueType;

{Инициализация очереди}

procedure Initialize;

begin

SetLength(queue, 0);

end;

{Функция IsEmpty возвращает True, если очередь пустая}

function IsEmpty: Boolean;

begin

IsEmpty := (Length(queue) = 0);

end;

{Извлечение целого числа из начала очереди}

procedure Dequeue(var value: Integer);

var

count: Integer;

begin

if IsEmpty then begin

value := 0;

Writeln(rus('Недопустимая операция извлечения -- Очередь пустая!'));

end

else begin

value := queue[Low(queue)];

for count := Low(queue) to (High(queue) - 1) do begin

queue[count] := queue[count + 1];

end;

SetLength(queue, Length(queue) - 1);

end;

end;

{Добавление целого значения в конец очереди}

procedure Enqueue(value: Integer);

begin

SetLength(queue, Length(queue) + 1);

queue[High(queue)] := value;

end;

{Тестирование очереди}

var

value, count: Integer;

begin

Initialize;

for count := 1 to 10 do begin

Enqueue(count);

Writeln(rus('Добавлено: ') + IntToStr(count));

end;

for count := 1 to 5 do begin

Dequeue(value);

Writeln(rus('Извлечено: ') + IntToStr(value));

end;

for count := 30 to 33 do begin

Enqueue(count);

Writeln(rus('Добавлено: ') + IntToStr(count));

end;

for count := 1 to 9 do begin

Dequeue(value);

Writeln(rus('Извлечено: ') + IntToStr(value));

end;

readln;

end.

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

Рассмотрим пример реализации на Object Pascal двусторонней очереди целых чисел. В коде программы DequeEx, реализующем двустороннюю очередь, используется двусвязный список, т. е. связный список, в котором каждый узел содержит два указателя: одни из них указывает на предыдущий узел, а другой – на следующий.

program DequeEx;

{$APPTYPE CONSOLE}

uses

SysUtils;

function Rus(mes: string):string;

// В ANSI русские буквы кодируются числами от 192 до 255,

// в ASCII - от 128 до 175 (А..Я, а..п) и от 224 до 239 (р..я)

var

i: integer; // номер обрабатываемого символа

begin

for i:=1 to length(mes) do

case mes[i] of

'А'..'п' : mes[i] := Chr(Ord(mes[i]) - 64);

'р'..'я' : mes[i] := Chr(Ord(mes [i]) - 16);

end;

rus := mes;

end;

type

NodePointer = ^NodeType;

NodeType = record

value: Integer;

next: NodePointer;

last: NodePointer;

end;

DequeType = record

front: NodePointer;

rear: NodePointer;

end;

{Инициализация двусторонней очереди}

procedure Initialize(var deque: DequeType);

begin

deque. front := nil;

deque. rear := nil;

end;

{Функция IsEmpty возвращает True, если очередь пустая}

function IsEmpty(deque: DequeType): Boolean;

begin

IsEmpty := (deque. front = nil) or (deque. rear = nil);

end;

{Добавление целого значения в начало двусторонней очереди}

procedure AddFront(var deque: DequeType; value: Integer);

var

temp: NodePointer;

begin

New(temp);

temp^.value := value;

temp^.next := deque. front;

temp^.last := nil;

if IsEmpty(deque) then begin

deque. rear := temp;

end

else begin

deque. front^.last := temp;

end;

deque. front := temp;

end;

{Добавление целого значения в конец двусторонней очереди}

procedure AddRear(var deque: DequeType; value: Integer);

var

temp: NodePointer;

begin

New(temp);

temp^.value := value;

temp^.last := deque. rear;

temp^.next := nil;

if IsEmpty(deque) then begin

deque. front := temp;

end

else begin

deque. rear^.next := temp;

end;

deque. rear := temp;

end;

{Извлечение целого значения из конца двусторонней очереди}

procedure RemoveFront(var deque: DequeType;

var value: Integer);

var

temp: NodePointer;

begin

if IsEmpty(deque) then begin

value := 0;

Writeln(rus('Недопустимый вызов функции RemoveFront() -- двусторонняя очередь пустая!'));

end

else begin

temp := deque. front;

value := temp^.value;

deque. front := temp^.next;

Dispose(temp);

if IsEmpty(deque) then begin

deque. rear := nil;

end

else begin

deque. front^.last := nil;

end;

end;

end;

{Извлечение целого значения из конца двусторонней очереди}

procedure RemoveRear(var deque: DequeType;

var value: Integer);

var

temp: NodePointer;

begin

if IsEmpty(deque) then begin

value := 0;

Writeln(rus('Недопустимый вызов функции RemoveRear() -- Двусторонняя очередь пустая!'));

end

else begin

temp := deque. rear;

value := temp^.value;

deque. rear := temp^.last;

Dispose(temp);

if IsEmpty(deque) then begin

deque. front := nil;

end

else begin

deque. rear^.next := nil;

end;

end;

end;

{Тестирование двусторонней очереди}

var

deque: DequeType;

count, value: Integer;

begin

Initialize(deque);

for count := 1 to 10 do begin

AddRear(deque, count);

Writeln(rus('Добавлено в конец: ') + IntToStr(count));

end;

for count := 5 downto 1 do begin

AddFront(deque, count);

Writeln(rus('Добавлено в начало: ') + IntToStr(count));

end;

for count := 1 to 10 do begin

RemoveFront(deque, value);

Writeln(rus('Извлечено из начала: ') + IntToStr(value));

end;

for count := 1 to 5 do begin

RemoveRear(deque, value);

Writeln(rus('Извлечено из конца: ') + IntToStr(value));

end;

readln

end.

Очереди с приоритетами, кучи и деревья

Очередь с приоритетами — это очередь, с каждым элементом которой ассоциирован некоторый приоритет. Обычно для очереди с приоритетами предусмотрены две операции: добавление но­вого элемента и извлечение элемента, имеющего самый низкий приоритет. Обратите внимание: в этом примере в первую очередь извлекаются элементы с низким приорите­том, т. е. значение термина "приоритет" инвертировано. В реальных задачах этот термин может применяться как в прямом, так и в инвер­тированном значении.

В программе PriorityQueueEx очередь с при­оритетами реализована на основе статического массива. В данном примере реализована так на­зываемая кольцевая очередь с приоритетами, т. е. очередь обладает способностью переходить из одного конца массива в другой. Процедура Insert() предназначена для вставки в очередь нового элемента соответственно его приоритету. Таким образом, элементы очереди всегда распо­лагаются в массиве таким образом, что с увели­чением значения индекса увеличивается при­оритет элементов (в восходящем порядке). Процедура DeleteMin() извлекает из очере­ди первый элемент, всегда имеющий минимальный приоритет.

Кольцевая очередь с приоритетами реализуется на основе статического массива. Ко­льцевая очередь может переходить из одного конца массива в другой. Например, при 50 элементах массива и 15 элементах очереди начало очереди front может располагаться в 46-м элементе, а конец rear – в 10-м. Процедура CheckBounds корректирует значения front и rear для реализации правильного перехода из конца массива в начало.

В программе для задания приоритета элементов используется генератор псевдослучайных чисел. Процедура Randomize() инициализирует встроенный в Delphi генератор значением, полученным от системных часов. Функция Random(x) возвращает случайное число целого типа, равномерно распределенное в диапазоне от 0 до х.

program PriorityQueueEx;

{$APPTYPE CONSOLE}

uses

SysUtils;

function Rus(mes: string):string;

// В ANSI русские буквы кодируются числами от 192 до 255,

// в ASCII - от 128 до 175 (А..Я, а..п) и от 224 до 239 (р..я)

var

i: integer; // номер обрабатываемого символа

begin

for i:=1 to length(mes) do

case mes[i] of

'А'..'п' : mes[i] := Chr(Ord(mes[i]) - 64);

'р'..'я' : mes[i] := Chr(Ord(mes [i]) - 16);

end;

rus := mes;

end;

const

PQ_MIN = 1; {Начальный индекс}

PQ_MAX = 50; {Конечный индекс}

PQ_SIZE = PQ_MAX - PQ_MIN + 1; {Максимальная длина

очереди с приоритетами}

type

PQRec = record

task: String[20];

priority: Integer;

end;

PQArray = array[PQ_MIN..PQ_MAX] of PQRec;

PQType = record

pq: PQArray; {Очередь с приоритетами}

front: Integer; {Начальный индекс очереди}

rear: Integer; {Конечный индекс очереди}

size: Integer; {Длина очереди с приоритетами}

end;

procedure Initialize(var pqueue: PQType);

begin

with pqueue do begin

front := PQ_MIN;

rear := PQ_MIN;

size := 0;

end;

end;

{Функция IsEmpty возвращает True, если очередь с приоритетами пустая}

function IsEmpty(pqueue: PQType): Boolean;

begin

IsEmpty := (pqueue. size = 0);

end;

{Функция IsFull возвращает True, если очередь с приоритетами заполнена}

function IsFull(pqueue: PQType): Boolean;

begin

IsFull := (pqueue. size = PQ_SIZE);

end;

{Проверка допустимости индекса массива}

procedure CheckBounds(var index: Integer);

begin

if (index < PQ_MIN) then begin

index := PQ_MAX;

end

else if (index > PQ_MAX) then begin

index := PQ_MIN;

end;

end;

{Вставка элемента в очередь с приоритетами}

procedure Insert(var pqueue: PQType; data: PQRec);

var

loc, prev: Integer;

found: Boolean;

begin

if IsFull(pqueue) then begin

writeln(rus('Ошибка при вызове Insert() -- очередь заполнена!'));

end

else begin

Inc(pqueue. size);

if (pqueue. size = 1) then begin

pqueue. front := pqueue. rear;

pqueue. pq[pqueue. rear] := data;

end

else begin

Inc(pqueue. rear);

CheckBounds(pqueue. rear);

found := False;

loc := pqueue. rear;

repeat

prev := loc - 1;

CheckBounds(prev);

if (data. priority < pqueue. pq[prev].priority) then begin

pqueue. pq[loc] := pqueue. pq[prev];

loc := prev;

end

else begin

found := True;

end;

until found or (loc = pqueue. front);

pqueue. pq[loc] := data;

end;

end;

end;

{Извлечение из очереди с приоритетами элемента с наименьшим приоритетом}

procedure DeleteMin(var pqueue: PQType; var data: PQRec);

begin

if IsEmpty(pqueue) then begin

data. task := '';

data. priority := 0;

Writeln(rus('Ошибка при вызове DeleteMin() -- очередь пустая!'));

end

else begin

data := pqueue. pq[pqueue. front];

Inc(pqueue. front);

CheckBounds(pqueue. front);

Dec(pqueue. size);

end;

end;

var

pqueue: PQType;

data: PQRec;

count: Integer;

begin

Writeln(rus('ВСТАВКА ЭЛЕМЕНТОВ:'));

Randomize;

Initialize(pqueue);

for count := 1 to 20 do begin

data. task := 'Задача ' + IntToStr(count);

data. priority := Random(10);

Insert(pqueue, data);

Writeln(rus('Вставлено: ') + rus(data. task) +

Rus(', Приоритет ') +

IntToStr(data. priority));

end;

Writeln('');

Writeln(Rus('ИЗВЛЕЧЕНИЕ ЭЛЕМЕНТОВ:'));

for count := 1 to 20 do begin

DeleteMin(pqueue, data);

Writeln(rus('Извлечено: ') + rus(data. task) +

Rus(', Приоритет ') +

IntToStr(data. priority));

end;

readln;

end.

Куча (или бинарная куча, binary heap) – это очередь с приоритетами, структурирован­ная как бинарное дерево. Чтобы понять, что такое кучи, необходимо сначала ознакомить­ся с древовидными структурами. Дерево – это набор узлов, между которыми установле­ны родительско-дочерние отношения. В древовидной структуре каждый узел может быть соединен с произвольным количеством дочерних узлов и только с одним родительским. Кроме того, каждое дерево содержит единственный узел, называющийся корневым узлом или корнем.

Бинарным называется дерева, каждый узел которого соединен не более чем с двумя дочерними узлами.

Рассмотрим следующий пример. В программе BinaryTreeEx для создания бинарного дерева, состоящего из 20 целых случайных чисел в диапазоне от 0 до 99, используй указатели. Программа выводит минимальное и максимальное целые значения, хранящиеся в дереве. Дерево организовано таким образом, что для каждого узла его левый дочерний узел содержит меньшее значение, чем родительский, а правый – большее или равное значению в родительском узле.

program BinaryTreeEx;

{$APPTYPE CONSOLE}

uses

SysUtils;

function Rus(mes: string):string;

// В ANSI русские буквы кодируются числами от 192 до 255,

// в ASCII - от 128 до 175 (А..Я, а..п) и от 224 до 239 (р..я)

var

i: integer; // номер обрабатываемого символа

begin

for i:=1 to length(mes) do

case mes[i] of

'А'..'п' : mes[i] := Chr(Ord(mes[i]) - 64);

'р'..'я' : mes[i] := Chr(Ord(mes [i]) - 16);

end;

rus := mes;

end;

type

NodePointer = ^NodeType;

NodeType = record

value: Integer;

left: NodePointer;

right: NodePointer;

end;

{Инициализация дерева}

procedure Initialize(var tree: NodePointer);

begin

tree := nil;

end;

{Вставка узла в дерево}

procedure Insert(var tree: NodePointer; data: Integer);

var

temp, loc, prev: NodePointer;

begin

New(temp);

temp^.value := data;

temp^.left := nil;

temp^.right := nil;

if (tree = nil) then begin

tree := temp;

end

else begin

loc := tree;

while (loc <> nil) do begin

prev := loc;

if (data < loc^.value) then begin

loc := loc^.left;

end

else begin

loc := loc^.right;

end;

end;

if data < prev^.value then begin

prev^.left := temp;

end

else begin

prev^.right := temp;

end;

end;

end;

{Получение минимального значения в дереве}

function GetMin(tree: NodePointer): Integer;

var

prev: NodePointer;

begin

prev := nil;

while (tree <> nil) do begin

prev := tree;

tree := tree^.left;

end;

if (prev = nil) then begin

GetMin := -1;

Writeln(Rus('Ошибка при вызове GetMin() -- дерево пустое!'));

end

else begin

GetMin := prev^.value;

end;

end;

{Получение максимального значения в дереве}

function GetMax(tree: NodePointer): Integer;

var

prev: NodePointer;

begin

prev := nil;

while (tree <> nil) do begin

prev := tree;

tree := tree^.right;

end;

if (prev = nil) then begin

GetMax := -1;

Writeln(Rus('Ошибка при вызове GetMax() -- дерево пустое!'));

end

else begin

GetMax := prev^.value;

end;

end;

var

tree: NodePointer;

count, value: Integer;

begin

Writeln(Rus('СОЗДАНИЕ ДЕРЕВА:'));

Randomize;

Initialize(tree);

for count := 1 to 20 do begin

value := Random(100);

Insert(tree, value);

Writeln(Rus('Вставлено: ') + IntToStr(value));

end;

Writeln('');

Writeln(Rus('ПОИСК МИНИМАЛЬНОГО И МАКСИМАЛЬНОГО ЗНАЧЕНИЙ:'));

Writeln(Rus('Минимальное значение: ') + IntToStr(GetMin(tree)));

Writeln(Rus('Максимальное значение: ') + IntToStr(GetMax(tree)));

readln;

end.

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

Для простейшей реализации кучи достаточно статического массива. Если родительский узел имеет индекс i, то дочерние узлы расположены в элементах с индексами 2i и 2i+1. Пример кучи с такой структурой реализован в программе НеарЕх. Назначение процедур Insert() и DeleteMin() описано в комментариях, содержащихся в исходном коде программы.

program HeapEx;

{$APPTYPE CONSOLE}

uses

SysUtils;

function Rus(mes: string):string;

// В ANSI русские буквы кодируются числами от 192 до 255,

// в ASCII - от 128 до 175 (А..Я, а..п) и от 224 до 239 (р..я)

var

i: integer; // номер обрабатываемого символа

begin

for i:=1 to length(mes) do

case mes[i] of

'А'..'п' : mes[i] := Chr(Ord(mes[i]) - 64);

'р'..'я' : mes[i] := Chr(Ord(mes [i]) - 16);

end;

rus := mes;

end;

const

HEAPSIZE = 50; {Максимальный размер кучи}

SENTINEL = -100; {Контрольное значение в заголовочном узле}

type

HeapRec = record

task: String[20];

priority: Integer;

end;

HeapArray = array[0..HEAPSIZE] of HeapRec;

HeapType = record

data: HeapArray; {Тип кучи}

size: Integer; {Количество элементов в куче}

end;

{Инициализация кучи}

procedure Initialize(var heap: HeapType);

begin

heap. size := 0;

with heap. data[0] do begin

task := Rus('ЗАГОЛОВОЧНЫЙ УЗЕЛ');

priority := SENTINEL;

end;

end;

{Функция IsEmpty возвращает True, если куча пустая}

function IsEmpty(heap: HeapType): Boolean;

begin

IsEmpty := (heap. size = 0);

end;

{Функция IsFull возвращает True, если куча заполнена}

function IsFull(heap: HeapType): Boolean;

begin

IsFull := (heap. size = HEAPSIZE);

end;

{Вставка элемента в кучу}

procedure Insert(var heap: HeapType; data: HeapRec);

var

i: Integer;

begin

if IsFull(heap) then begin

Writeln(rus('Ошибка при вызове Insert() -- куча заполнена!'));

end

else begin

Inc(heap. size);

i := heap. size;

while (heap. data[i div 2].priority > data. priority) do begin

heap. data[i] := heap. data[i div 2];

i := i div 2;

end;

heap. data[i] := data;

end;

end;

{Извлечение из кучи следующего элемента с минимальным приоритетом}

procedure DeleteMin(var heap: HeapType; var data: HeapRec);

var

i, child: Integer;

lastElement: HeapRec;

done: Boolean;

begin

if IsEmpty(heap) then begin

data. task := '';

data. priority := 0;

Writeln(Rus('Ошибка при вызове DeleteMin() -- куча пустая!'));

end

else begin

data := heap. data[1];

lastElement := heap. data[heap. size];

Dec(heap. size);

i := 1;

done := False;

while (i * 2 <= heap. size) and not(done) do begin

{Поиск дочернего узла с меньшим значением}

child := i * 2;

if (child <> heap. size) then begin

if heap. data[child + 1].priority <

heap. data[child].priority then begin

Inc(child);

end;

end;

{Перемещение элементов кучи}

if lastElement. priority > heap. data[child].priority

then begin

heap. data[i] := heap. data[child];

i := child;

end

else begin

done := True;

end;

end;

heap. data[i] := lastElement;

end;

end;

{Тестирование кучи}

var

heap: HeapType;

data: HeapRec;

count: Integer;

begin

Randomize;

Writeln(Rus('ВСТАВКА ЭЛЕМЕНТОВ:'));

Initialize(heap);

for count := 1 to 20 do begin

data. task := Rus('Задача ') + IntToStr(count);

data. priority := Random(10);

Insert(heap, data);

Writeln(Rus('Вставлено: ') + data. task +

Rus(', Приоритет ') +

IntToStr(data. priority));

end;

Writeln('');

Writeln(Rus('ИЗВЛЕЧЕНИЕ ЭЛЕМЕНТОВ:'));

for count := 1 to 20 do begin

DeleteMin(heap, data);

Writeln(Rus('Извлечено: ') + data. task +

Rus(', Приоритет ') +

IntToStr(data. priority));

end;

readln;

end.

Контрольные вопросы.

Что называется указателем? Для чего необходимо значение nil при работе с указателями? Назовите операции, которые допускаются над значениями ссылочного типа? Какие стандартные процедура реализуют основные действия над динамическими переменными? К чему приводит «потеря» указателя на данные, хранимые в динамической памяти? Ограничена ли динамическая память?

Задания

Реализуйте операции работы со стеком, построенным на основе динамических структур. Реализуйте операции работы с очередью, построенной на основе динамических структур.

Основные порталы (построено редакторами)

Домашний очаг

ДомДачаСадоводствоДетиАктивность ребенкаИгрыКрасотаЖенщины(Беременность)СемьяХобби
Здоровье: • АнатомияБолезниВредные привычкиДиагностикаНародная медицинаПервая помощьПитаниеФармацевтика
История: СССРИстория РоссииРоссийская Империя
Окружающий мир: Животный мирДомашние животныеНасекомыеРастенияПриродаКатаклизмыКосмосКлиматСтихийные бедствия

Справочная информация

ДокументыЗаконыИзвещенияУтверждения документовДоговораЗапросы предложенийТехнические заданияПланы развитияДокументоведениеАналитикаМероприятияКонкурсыИтогиАдминистрации городовПриказыКонтрактыВыполнение работПротоколы рассмотрения заявокАукционыПроектыПротоколыБюджетные организации
МуниципалитетыРайоныОбразованияПрограммы
Отчеты: • по упоминаниямДокументная базаЦенные бумаги
Положения: • Финансовые документы
Постановления: • Рубрикатор по темамФинансыгорода Российской Федерациирегионыпо точным датам
Регламенты
Термины: • Научная терминологияФинансоваяЭкономическая
Время: • Даты2015 год2016 год
Документы в финансовой сферев инвестиционнойФинансовые документы - программы

Техника

АвиацияАвтоВычислительная техникаОборудование(Электрооборудование)РадиоТехнологии(Аудио-видео)(Компьютеры)

Общество

БезопасностьГражданские права и свободыИскусство(Музыка)Культура(Этика)Мировые именаПолитика(Геополитика)(Идеологические конфликты)ВластьЗаговоры и переворотыГражданская позицияМиграцияРелигии и верования(Конфессии)ХристианствоМифологияРазвлеченияМасс МедиаСпорт (Боевые искусства)ТранспортТуризм
Войны и конфликты: АрмияВоенная техникаЗвания и награды

Образование и наука

Наука: Контрольные работыНаучно-технический прогрессПедагогикаРабочие программыФакультетыМетодические рекомендацииШколаПрофессиональное образованиеМотивация учащихся
Предметы: БиологияГеографияГеологияИсторияЛитератураЛитературные жанрыЛитературные героиМатематикаМедицинаМузыкаПравоЖилищное правоЗемельное правоУголовное правоКодексыПсихология (Логика) • Русский языкСоциологияФизикаФилологияФилософияХимияЮриспруденция

Мир

Регионы: АзияАмерикаАфрикаЕвропаПрибалтикаЕвропейская политикаОкеанияГорода мира
Россия: • МоскваКавказ
Регионы РоссииПрограммы регионовЭкономика

Бизнес и финансы

Бизнес: • БанкиБогатство и благосостояниеКоррупция(Преступность)МаркетингМенеджментИнвестицииЦенные бумаги: • УправлениеОткрытые акционерные обществаПроектыДокументыЦенные бумаги - контрольЦенные бумаги - оценкиОблигацииДолгиВалютаНедвижимость(Аренда)ПрофессииРаботаТорговляУслугиФинансыСтрахованиеБюджетФинансовые услугиКредитыКомпанииГосударственные предприятияЭкономикаМакроэкономикаМикроэкономикаНалогиАудит
Промышленность: • МеталлургияНефтьСельское хозяйствоЭнергетика
СтроительствоАрхитектураИнтерьерПолы и перекрытияПроцесс строительстваСтроительные материалыТеплоизоляцияЭкстерьерОрганизация и управление производством