Программирование на языке высокого уровня Паскаль

Работа с динамической памятью

Разбить на страницы
Показывать лекцию целиком

Презентацию к данной работе Вы можете скачать здесь.

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

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

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

Указатели

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

Указатели в Паскале можно разделить на два вида: стандартные и определяемые программистом. Величины стандартного типа pointer предназначены для хранения адресов данных произвольного типа, например:

var p : pointer;

Программист может определить указатель на данные или подпрограмму конкретного типа. Как и для других нестандартных типов, это делается в разделе type:

type pword = ^word; { читается как 'указатель на word' }
...
var pw : pword;

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

Аналогично другим типам, тип указателя на данные можно описать и непосредственно при описании переменной, например:

var pw : ^word;

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

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

  • Любому указателю можно присвоить стандартную константу nil, которая означает, что указатель не ссылается на какую-либо конкретную ячейку памяти.
  • Указатели стандартного типа pointer совместимы с указателями любого типа.
  • Указателю на конкретный тип данных можно присвоить только значение указателя того же или стандартного типа.
  • Операция @ и функция addr позволяют получить адрес переменной, например:

    var x  : word;        { переменная }
        pw : ^word;       { указатель на величины типа word }
    ...
    pw := @w;             { или pw := addr(w); }

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

    pw^ := 2;   
    inc(pw^);    
    writeln(pw^);

    В первом операторе в ячейку памяти, адрес которой хранится в переменной pw, заносится число 2. При выполнении оператора вывода на экране появится число 3.

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

    ПРИМЕЧАНИЕ Указатели стандартного типа разыменовывать нельзя.

    Указатели можно сравнивать на равенство и неравенство, например:

    if p1 = p2 then ...
    if p <> nil then ...

    Динамические переменные

    Динамические переменные создаются в хипе во время выполнения программы с помощью подпрограмм new или getmem. Динамические переменные не имеют собственных имен — к ним обращаются через указатели.

  • Процедура new (var p : тип_указателя) выделяет в динамической памяти участок размера, достаточного для размещения переменной того типа, на который ссылается указатель p, и заносит в него адрес начала этого участка.
  • Функция new(тип_указателя) : pointer выделяет в динамической памяти участок размера, достаточного для размещения переменной базового типа для заданного типа указателя, и возвращает адрес начала этого участка.

    Если в new использовать указатель стандартного типа pointer, память фактически не выделяется, а при попытке работать с ней выдается ошибка. Поэтому процедура и функция new обычно применяются для типизированных указателей.

  • Процедура getmem (var p : pointer; size : word) выделяет в динамической памяти участок размером в size байт и присваивает адрес его начала указателю p. Эту процедуру можно применять и для указателей типа pointer, поскольку количество выделяемой памяти задается в явном виде.
  • Если выделить требуемый объем памяти не удалось, программа аварийно завершается.

    Рассмотрим пример работы с динамическими переменными. Определим в разделе описания переменных главной программы три указателя p1, p2 и p3.

    type rec = record
            d : word;
            s : string;
         end;
         pword = ^word;
    var  p1, p2 : pword;
         p3     : ^rec;

    Это — обычные статические переменные, компилятор выделяет под них в сегменте данных по четыре байта и обнуляет их (рис 5.1).

    (рис 5.1) Размещение указателей в памяти

    В разделе исполняемых операторов программы запишем операторы:

    new(p1); p2 := new(pword); new(p3);

    В результате выполнения процедуры new(p1) в хипе выделяется объем памяти, достаточный для размещения переменной типа word, и адрес начала этого участка памяти записывается в переменную p1. Второй оператор выполняет аналогичные действия, но используется функция new. При вызове процедуры new с параметром p3 в динамической памяти будет выделено количество байтов, достаточное для размещения записи типа rec.

    Доступ к выделенным областям осуществляется с помощью операции разадресации:

    p1^ := 2; p2^ := 4; p3^.d := p1^; p3^.s := 'Вася';

    В этих операторах в выделенную память заносятся значения (рис 5.2).

    (рис 5.2) Выделение и заполнение динамической памяти

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

    inc(p1^); p2^ := p1^ + p3^.d;
    with p3^ do writeln (d, s);
    ). Это приводит к появлению так называемого мусора (на рисунке обозначен овалом), когда доступа к участку динамической памяти нет, а сам он помечен как занятый.

    (рис 5.3) Мусор

    Для освобождения динамической памяти используются процедуры Dispose и Freemem, причем если память выделялась с помощью new, следует применять Dispose, в противном случае — Freemem.

  • Процедура Dispose (var p : pointer) освобождает участок памяти, выделенный для размещения динамической переменной процедурой или функцией New, и значение указателя p становится неопределенным.
  • Процедура Freemem (var p : pointer; size : word) освобождает участок памяти размером size, начиная с адреса, находящегося в p. Значение указателя становится неопределенным.
  • При завершении программы используемая ею динамическая память освобождается автоматически, поэтому явным образом освобождать ненужную память необходимо только в том случае, если она может потребоваться при дальнейшем выполнении программы.

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

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

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

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

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

    type 
        pnode =  ^node;
        node = record
            d : word;                           { информационная }
            s : string;                         {          часть }
            p : pnode;          { указатель на следующий элемент }
         end;
    ПРИМЕЧАНИЕ Обратите внимание, что тип указателя pnode на запись node определен раньше, чем сама запись. Это не противоречит принципу "использование только после описания", поскольку для описания переменной типа pnode информации вполне достаточно.

    Рассмотрим принципы работы с основными динамическими структурами.

    Стеки

    Стек является простейшей динамической структурой. Добавление элементов в стек и выборка из него выполняются из одного конца, называемого вершиной стека. Другие операции со стеком не определены. При выборке элемент исключается из стека.

    Говорят, что стек реализует принцип обслуживания LIFO (last in — first out, последним пришел — первым обслужен). Стеки широко применяются в системном программном обеспечении, компиляторах, в различных рекурсивных алгоритмах.

    Для работы со стеком используются две статические переменные: указатель на вершину стека и вспомогательный указатель.

    var top, p : pnode;

    Тип указателей должен соответствовать типу элементов стека.

    В приведена программа, которая формирует стек из пяти целых чисел и их текстового представления и выводит его на экран. Функция занесения в стек по традиции называется push, а функция выборки — pop.

    program stack;
    const n = 5;
    type pnode = ^node;
         node = record                                { элемент стека }
             d : word;
             s : string;
             p : pnode;
         end;
    var  top : pnode;                    { указатель на вершину стека }
         i   : word;
         s   : string;
    const text : array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
    { ------------------------------ занесение в стек --------------------------- }
    function push(top : pnode; d : word; const s : string) : pnode;
    var p : pnode;
    begin
        new(p);
        p^.d := d; p^.s := s;  p^.p := top;
        push := p;
    end;
    { ------------------------------ выборка из стека --------------------------- }
    function pop(top : pnode; var d : word; var s : string) : pnode;
    var p : pnode;
    begin
        d := top^.d; s := top^.s;
        pop := top^.p;
        dispose(top);
    end;
    { ------------------------------- главная программа ----------------------------- }
    begin
        top := nil;
        for i := 1 to n do top := push(top, i, text[i]);        { занесение в стек: }
        while top <> nil do begin                               { выборка из стека: }
            top := pop(top, i, s);     writeln(i:2, s);
        end;
    end.

    Очереди

    Очередь — это динамическая структура данных, добавление элементов в которую выполняется в один конец, а выборка — из другого конца. Другие операции с очередью не определены. При выборке элемент исключается из очереди. Говорят, что очередь реализует принцип обслуживания FIFO (first in — first out, первым пришел — первым обслужен). В программировании очереди применяются очень широко — например, при моделировании, буферизованном вводе-выводе или диспетчеризации задач в операционной системе.

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

    var beg, fin, p : pnode;

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

    В приведена программа, которая формирует очередь из пяти целых чисел и их текстового представления и выводит еe на экран. Для разнообразия операции с очередью оформлены в виде процедур. Процедура начального формирования называется first, помещения в конец очереди — add, а выборки — get.

    program queue;
    const n = 5;
    type pnode = ^node;
         node = record                                            { элемент очереди }
             d : word; 
             s : string; 
             p : pnode;
         end;
    var  beg, fin : pnode;                    { указатели на начало и конец очереди }
        i         : word; 
        s         : string;
    const text : array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
    { ------------------ начальное формирование очереди --------------------------- }
    procedure first(var beg, fin : pnode; d : word; const s : string);
    begin
        new(beg);
        beg^.d := d; beg^.s := s;  beg^.p := nil;
        fin := beg;
    end;
    { --------------------- добавление элемента в конец --------------------------- }
    procedure add(var fin : pnode; d : word; const s : string);
    var p : pnode;
    begin
        new(p);
        p^.d := d; p^.s := s;  p^.p := nil;
        fin^.p := p;
        fin := p;
    end;
    { ---------------------- выборка элемента из начала --------------------------- }
    procedure get(var beg : pnode; var d : word; var s : string);
    var p : pnode;
    begin
        d := beg^.d; s := beg^.s;
        p := beg; beg := beg^.p; 
        dispose(p);
    end;
    { ------------------------------- главная программа --------------------------- }
    begin
        { занесение в очередь: }
        first(beg, fin, 1, text[1]); 
        for i := 2 to 5 do add(fin, i, text[i]);
        { выборка из очереди: }
        while beg <> nil do begin
            get(beg, i, s);
            writeln(i:2, s);
        end;
    end.

    Линейные списки

    В линейном списке каждый элемент связан со следующим и, возможно, с предыдущим. В первом случае список называется односвязным, во втором — двусвязным. Также применяются термины "однонаправленный" и "двунаправленный". Если последний элемент связать указателем с первым, получится кольцевой список.

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

    Над списками можно выполнять следующие операции:

  • начальное формирование списка (создание первого элемента);
  • добавление элемента в конец списка;
  • чтение элемента с заданным ключом;
  • вставка элемента в заданное место списка (до или после элемента с заданным ключом);
  • удаление элемента с заданным ключом;
  • упорядочивание списка по ключу.
  • Стек и очередь представляют собой частные случаи линейного списка с ограниченным набором допустимых операций.

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

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

    program linked_list;
    const n = 5;
    type pnode = ^node;
         node = record                                             { элемент списка }
             d : word; 
             s : string;
             p : pnode;
         end;
    var beg    : pnode;                                { указатель на начало списка }
        i, key : word;
        s      : string;
        option : word;
    const text: array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
    { -------------- добавление элемента в конец списка --------------------------- }
    procedure add(var beg : pnode; d : word; const s : string);
    var p : pnode;                               { указатель на создаваемый элемент }
        t : pnode;                                 { указатель для просмотра списка }
    begin
        new(p);                                                 { создание элемента }
        p^.d := d; p^.s := s;                                 { заполнение элемента }
        p^.p := nil;
        if beg = nil then beg := p                                { список был пуст }
        else begin                                                 { список не пуст }
            t := beg;
            while t^.p <> nil do                        { проход по списку до конца }
                t := t^.p;            
            t^.p := p;                      { привязка нового элемента к последнему }
        end
    end;
    { ------------------------- поиск элемента по ключу --------------------------- }
    function find(beg : pnode; key : word; var p, pp : pnode) : boolean;
    begin
        p := beg;
        while p <> nil do begin                                                 { 1 }
            if p^.d = key then begin                                            { 2 }
                find := true; exit end;
            pp := p;                                                            { 3 }
            p := p^.p;                                                          { 4 }
        end;
        find := false;
    end;
    
    { -------------------------------- вставка элемента --------------------------- }
    procedure insert(beg : pnode; key, d : word; const s : string);
    var p    : pnode;                            { указатель на создаваемый элемент }
        pkey : pnode;                                { указатель на искомый элемент }
        pp   : pnode;                             { указатель на предыдущий элемент }
    begin
        if not find(beg, key, pkey, pp) then begin
            writeln(' вставка не выполнена'); exit; end;
        new(p);                                                                 { 1 }
        p^.d := d; p^.s := s;                                                   { 2 }
        p^.p := pkey^.p;                                                        { 3 }
        pkey^.p := p;                                                           { 4 }
    end;
    { ------------------------------- удаление элемента --------------------------- }
    procedure del(var beg : pnode; key : word);
    var p  : pnode;         { указатель на удаляемый элемент }
        pp : pnode;         { указатель на предыдущий элемент }
    begin
        if not find(beg, key, p, pp) then begin 
            writeln(' удаление не выполнено'); exit; end; 
        if p = beg then beg := beg^.p                   { удаление первого элемента }
        else pp^.p := p^.p;
        dispose(p);
    end;
    { ------------------------------------ вывод списка --------------------------- }
    procedure print(beg : pnode);
    var p : pnode;                                 { указатель для просмотра списка }
    begin
        p := beg;
        while p <> nil do begin                                    { цикл по списку }
            writeln(p^.d:3, p^.s);                                 { вывод элемента }
            p := p^.p                        { переход к следующему элементу списка }
        end;
    end;
    { ------------------------------- главная программа --------------------------- }
    begin
        for i := 1 to 5 do add(beg, i, text[i]);
        while true do begin 
            writeln('1 - вставка, 2 - удаление, 3 - вывод, 4 - выход');
            readln(option);
            case option of
                1: begin                                                  { вставка }
                    writeln('Ключ для вставки?');
                    readln(key);
                    writeln('Вставляемый элемент?');
                    readln(i); readln(s);
                    insert(beg, key, i, s);
                end;
                2: begin                                                 { удаление }
                    writeln('Ключ для удаления?');   
                    readln(key);
                    del(beg, key);
                end;
                3: begin                                                    { вывод }
                    writeln('Вывод списка:');
                    print(beg);
                end;
                4: exit;                                                    { выход }
            end
            writeln;
        end
    end.

    Функция поиска элемента find возвращает true, если искомый элемент найден, и false в противном случае. Поскольку одного факта отыскания элемента недостаточно, функция также возвращает через список параметров два указателя: на найденный элемент p и на предшествующий ему pp. Последний требуется при удалении элемента из списка, поскольку при этом необходимо связывать предыдущий и последующий по отношению к удаляемому элементы.

    Сначала указатель p устанавливается на начало списка, и организуется цикл просмотра списка (оператор 1). Если поле данных очередного элемента совпало с заданным ключом (оператор 2), формируется признак успешного поиска и функция завершается. В противном случае перед переносом указателя на следующий элемент списка (он хранится в поле p текущего элемента, оператор 4) его значение запоминается в переменной pp (оператор 3) для того, чтобы при следующем проходе цикла в ней находился указатель на предыдущий элемент.

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

    Вставка элемента выполняется после элемента с заданным ключом (процедура insert ). Если с помощью функции find место вставки определить не удалось, выводится сообщение и процедура завершается (в этом случае можно было использовать и другой алгоритм — добавлять элемент к концу списка, это определяется конкретным предназначением программы). Если элемент найден, указатель на него заносится в переменную pkey.

    Под новый элемент выделяется место в динамической памяти (оператор 1), и информационные поля элемента заполняются переданными в процедуру значениями (оператор 2). Новый элемент ).

    (рис 5.4) Вставка элемента в список

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

    Бинарные деревья

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

    Пример бинарного дерева приведен на рис 5.5 (корень обычно изображается сверху). Узел, не имеющий поддеревьев, называется листом. Исходящие узлы называются предками, входящие — потомками. Высота дерева определяется количеством уровней, на которых располагаются его узлы.

    (рис 5.5) Пример бинарного дерева

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

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

    procedure print_tree( дерево );
    begin
        print_tree( левое_поддерево )
        посещение корня
        print_tree( правое_поддерево )
    end;

    Эта процедура позволяет получить последовательность ключей, отсортированную по возрастанию. Результат обхода дерева, изображенного на рис 5.5:

    1, 6, 8, 10, 20, 21, 25, 30

    Если в функции обхода первое обращение идет к правому поддереву, результат обхода будет другим:

    30, 25, 21, 20, 10, 8, 6, 1

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

    Для бинарных деревьев определены операции:

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

    type pnode = ^node;
         node = record
             data  : word;                     { ключ }
             left  : pnode;                    { указатель на левое поддерево }
             right : pnode                     { указатель на правое поддерево }
            end;

    Доступ к дереву в программе осуществляется через указатель на его корень:

    var root : pnode;

    Рассмотрим сначала ).

    function find(root : pnode; key : word; var p, parent : pnode) : boolean;
    begin
        p := root;                             { поиск начинается от корня }
        while p <> nil do begin
            if key = p^.data then              { узел с таким ключом есть }
                begin find := true; exit end;    
            parent := p;                       { запомнить указатель перед спуском }
            if key < p^.data
                then p := p^.left              { спуститься влево }
                else p := p^.right;            { спуститься вправо }
        end;
        find := false;
    end;

    Функция возвращает булевский признак успешности поиска. Ей передаются указатель на корень дерева, в котором выполняется поиск ( root ), и искомый ключ ( key ). Выходными параметрами функции являются указатели на найденный элемент ( p ) и его предка ( parent ).

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

    В приведен пример программы работы с бинарным деревом.

    program bintree;
    uses crt;
    type pnode = ^node;
         node = record
             data  : word;        { ключ }
             left  : pnode;       { указатель на левое поддерево }
             right : pnode        { указатель на правое поддерево }
         end;
    var root   : pnode;
        key    : word;
        option : word;
    { ------------------------------------ вывод дерева --------------------------- }
    procedure print_tree(p : pnode; level : integer);
    var i : integer;
    begin
        if p = nil then exit;
        with p^ do begin
            print_tree(right, level + 1);
            for i := 1 to level do write('     ');
            writeln(data);
            print_tree(left, level + 1);
        end
    end;
    { ------------------------------- поиск по дереву – см. рис. 5.5----------- }
    function find(root : pnode; key : word; var p, parent : pnode) : boolean;
    { ---------------------------- включение в дерево ----------------------------- }
    procedure insert(var root : pnode; key : word);
    var p, parent : pnode;
    begin
        if find(root, key, p, parent) then begin
            writeln(' такой элемент уже есть'); exit; end;
        new(p);                                          { создание нового элемента }
        p^.data  := key;
        p^.left  := nil;
        p^.right := nil;
        if root = nil then root := p                               { первый элемент }
        else                                { присоединение нового элемента к дереву}
            if key < parent^.data
                then parent^.left  := p
                else parent^.right := p;
    end;
    { ------------------------------ удаление из дерева - см. учебник     --------- }
    procedure del(var root : pnode; key : word); 
    { ------------------------------- главная программа --------------------------- }
    begin
        root := nil;
        while true do begin
            writeln('1 - вставка, 2 - удаление, 3 - вывод, 4 - выход');
            readln(option);
            case option of
                1: begin                                                  { вставка }
                        writeln('Введите ключ для вставки: '); readln(key);
                        insert(root, key);
                    end;
                2: begin                                                 { удаление }
                        writeln('Введите ключ для удаления: '); readln(key);
                        del(root, key);
                    end;
                3: begin                                                    { вывод }
                        clrscr;
                        if root = nil then writeln ('дерево пустое')
                        else print_tree(root, 0);
                    end;
                4: exit;                                                    { выход }
            end;
            writeln;
        end
    end.

    Рассмотрим функцию обхода дерева print_tree. Вторым параметром в нее передается целая переменная, определяющая, на каком уровне находится узел. Корень находится на уровне 0. Дерево печатается по горизонтали так, что корень находится слева. Для дерева, изображенного на рис 5.5, вывод выглядит так:

    30    
        25
                21
            20
    10
            8
        6
            1
    Страницы:

    Презентацию к данной работе Вы можете скачать здесь.

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

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

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

    Указатели

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

    Указатели в Паскале можно разделить на два вида: стандартные и определяемые программистом. Величины стандартного типа pointer предназначены для хранения адресов данных произвольного типа, например:

    var p : pointer;

    Программист может определить указатель на данные или подпрограмму конкретного типа. Как и для других нестандартных типов, это делается в разделе type:

    type pword = ^word; { читается как 'указатель на word' }
    ...
    var pw : pword;

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

    Аналогично другим типам, тип указателя на данные можно описать и непосредственно при описании переменной, например:

    var pw : ^word;

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

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

  • Любому указателю можно присвоить стандартную константу nil, которая означает, что указатель не ссылается на какую-либо конкретную ячейку памяти.
  • Указатели стандартного типа pointer совместимы с указателями любого типа.
  • Указателю на конкретный тип данных можно присвоить только значение указателя того же или стандартного типа.
  • Операция @ и функция addr позволяют получить адрес переменной, например:

    var x  : word;        { переменная }
        pw : ^word;       { указатель на величины типа word }
    ...
    pw := @w;             { или pw := addr(w); }

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

    pw^ := 2;   
    inc(pw^);    
    writeln(pw^);

    В первом операторе в ячейку памяти, адрес которой хранится в переменной pw, заносится число 2. При выполнении оператора вывода на экране появится число 3.

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

    ПРИМЕЧАНИЕ Указатели стандартного типа разыменовывать нельзя.

    Указатели можно сравнивать на равенство и неравенство, например:

    if p1 = p2 then ...
    if p <> nil then ...

    Динамические переменные

    Динамические переменные создаются в хипе во время выполнения программы с помощью подпрограмм new или getmem. Динамические переменные не имеют собственных имен — к ним обращаются через указатели.

  • Процедура new (var p : тип_указателя) выделяет в динамической памяти участок размера, достаточного для размещения переменной того типа, на который ссылается указатель p, и заносит в него адрес начала этого участка.
  • Функция new(тип_указателя) : pointer выделяет в динамической памяти участок размера, достаточного для размещения переменной базового типа для заданного типа указателя, и возвращает адрес начала этого участка.

    Если в new использовать указатель стандартного типа pointer, память фактически не выделяется, а при попытке работать с ней выдается ошибка. Поэтому процедура и функция new обычно применяются для типизированных указателей.

  • Процедура getmem (var p : pointer; size : word) выделяет в динамической памяти участок размером в size байт и присваивает адрес его начала указателю p. Эту процедуру можно применять и для указателей типа pointer, поскольку количество выделяемой памяти задается в явном виде.
  • Если выделить требуемый объем памяти не удалось, программа аварийно завершается.

    Рассмотрим пример работы с динамическими переменными. Определим в разделе описания переменных главной программы три указателя p1, p2 и p3.

    type rec = record
            d : word;
            s : string;
         end;
         pword = ^word;
    var  p1, p2 : pword;
         p3     : ^rec;

    Это — обычные статические переменные, компилятор выделяет под них в сегменте данных по четыре байта и обнуляет их (рис 5.1).

    (рис 5.1) Размещение указателей в памяти

    В разделе исполняемых операторов программы запишем операторы:

    new(p1); p2 := new(pword); new(p3);

    В результате выполнения процедуры new(p1) в хипе выделяется объем памяти, достаточный для размещения переменной типа word, и адрес начала этого участка памяти записывается в переменную p1. Второй оператор выполняет аналогичные действия, но используется функция new. При вызове процедуры new с параметром p3 в динамической памяти будет выделено количество байтов, достаточное для размещения записи типа rec.

    Доступ к выделенным областям осуществляется с помощью операции разадресации:

    p1^ := 2; p2^ := 4; p3^.d := p1^; p3^.s := 'Вася';

    В этих операторах в выделенную память заносятся значения (рис 5.2).

    (рис 5.2) Выделение и заполнение динамической памяти

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

    inc(p1^); p2^ := p1^ + p3^.d;
    with p3^ do writeln (d, s);
    ). Это приводит к появлению так называемого мусора (на рисунке обозначен овалом), когда доступа к участку динамической памяти нет, а сам он помечен как занятый.

    (рис 5.3) Мусор

    Для освобождения динамической памяти используются процедуры Dispose и Freemem, причем если память выделялась с помощью new, следует применять Dispose, в противном случае — Freemem.

  • Процедура Dispose (var p : pointer) освобождает участок памяти, выделенный для размещения динамической переменной процедурой или функцией New, и значение указателя p становится неопределенным.
  • Процедура Freemem (var p : pointer; size : word) освобождает участок памяти размером size, начиная с адреса, находящегося в p. Значение указателя становится неопределенным.
  • При завершении программы используемая ею динамическая память освобождается автоматически, поэтому явным образом освобождать ненужную память необходимо только в том случае, если она может потребоваться при дальнейшем выполнении программы.

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

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

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

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

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

    type 
        pnode =  ^node;
        node = record
            d : word;                           { информационная }
            s : string;                         {          часть }
            p : pnode;          { указатель на следующий элемент }
         end;
    ПРИМЕЧАНИЕ Обратите внимание, что тип указателя pnode на запись node определен раньше, чем сама запись. Это не противоречит принципу "использование только после описания", поскольку для описания переменной типа pnode информации вполне достаточно.

    Рассмотрим принципы работы с основными динамическими структурами.

    Стеки

    Стек является простейшей динамической структурой. Добавление элементов в стек и выборка из него выполняются из одного конца, называемого вершиной стека. Другие операции со стеком не определены. При выборке элемент исключается из стека.

    Говорят, что стек реализует принцип обслуживания LIFO (last in — first out, последним пришел — первым обслужен). Стеки широко применяются в системном программном обеспечении, компиляторах, в различных рекурсивных алгоритмах.

    Для работы со стеком используются две статические переменные: указатель на вершину стека и вспомогательный указатель.

    var top, p : pnode;

    Тип указателей должен соответствовать типу элементов стека.

    В приведена программа, которая формирует стек из пяти целых чисел и их текстового представления и выводит его на экран. Функция занесения в стек по традиции называется push, а функция выборки — pop.

    program stack;
    const n = 5;
    type pnode = ^node;
         node = record                                { элемент стека }
             d : word;
             s : string;
             p : pnode;
         end;
    var  top : pnode;                    { указатель на вершину стека }
         i   : word;
         s   : string;
    const text : array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
    { ------------------------------ занесение в стек --------------------------- }
    function push(top : pnode; d : word; const s : string) : pnode;
    var p : pnode;
    begin
        new(p);
        p^.d := d; p^.s := s;  p^.p := top;
        push := p;
    end;
    { ------------------------------ выборка из стека --------------------------- }
    function pop(top : pnode; var d : word; var s : string) : pnode;
    var p : pnode;
    begin
        d := top^.d; s := top^.s;
        pop := top^.p;
        dispose(top);
    end;
    { ------------------------------- главная программа ----------------------------- }
    begin
        top := nil;
        for i := 1 to n do top := push(top, i, text[i]);        { занесение в стек: }
        while top <> nil do begin                               { выборка из стека: }
            top := pop(top, i, s);     writeln(i:2, s);
        end;
    end.

    Очереди

    Очередь — это динамическая структура данных, добавление элементов в которую выполняется в один конец, а выборка — из другого конца. Другие операции с очередью не определены. При выборке элемент исключается из очереди. Говорят, что очередь реализует принцип обслуживания FIFO (first in — first out, первым пришел — первым обслужен). В программировании очереди применяются очень широко — например, при моделировании, буферизованном вводе-выводе или диспетчеризации задач в операционной системе.

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

    var beg, fin, p : pnode;

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

    В приведена программа, которая формирует очередь из пяти целых чисел и их текстового представления и выводит еe на экран. Для разнообразия операции с очередью оформлены в виде процедур. Процедура начального формирования называется first, помещения в конец очереди — add, а выборки — get.

    program queue;
    const n = 5;
    type pnode = ^node;
         node = record                                            { элемент очереди }
             d : word; 
             s : string; 
             p : pnode;
         end;
    var  beg, fin : pnode;                    { указатели на начало и конец очереди }
        i         : word; 
        s         : string;
    const text : array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
    { ------------------ начальное формирование очереди --------------------------- }
    procedure first(var beg, fin : pnode; d : word; const s : string);
    begin
        new(beg);
        beg^.d := d; beg^.s := s;  beg^.p := nil;
        fin := beg;
    end;
    { --------------------- добавление элемента в конец --------------------------- }
    procedure add(var fin : pnode; d : word; const s : string);
    var p : pnode;
    begin
        new(p);
        p^.d := d; p^.s := s;  p^.p := nil;
        fin^.p := p;
        fin := p;
    end;
    { ---------------------- выборка элемента из начала --------------------------- }
    procedure get(var beg : pnode; var d : word; var s : string);
    var p : pnode;
    begin
        d := beg^.d; s := beg^.s;
        p := beg; beg := beg^.p; 
        dispose(p);
    end;
    { ------------------------------- главная программа --------------------------- }
    begin
        { занесение в очередь: }
        first(beg, fin, 1, text[1]); 
        for i := 2 to 5 do add(fin, i, text[i]);
        { выборка из очереди: }
        while beg <> nil do begin
            get(beg, i, s);
            writeln(i:2, s);
        end;
    end.

    Линейные списки

    В линейном списке каждый элемент связан со следующим и, возможно, с предыдущим. В первом случае список называется односвязным, во втором — двусвязным. Также применяются термины "однонаправленный" и "двунаправленный". Если последний элемент связать указателем с первым, получится кольцевой список.

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

    Над списками можно выполнять следующие операции:

  • начальное формирование списка (создание первого элемента);
  • добавление элемента в конец списка;
  • чтение элемента с заданным ключом;
  • вставка элемента в заданное место списка (до или после элемента с заданным ключом);
  • удаление элемента с заданным ключом;
  • упорядочивание списка по ключу.
  • Стек и очередь представляют собой частные случаи линейного списка с ограниченным набором допустимых операций.

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

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

    program linked_list;
    const n = 5;
    type pnode = ^node;
         node = record                                             { элемент списка }
             d : word; 
             s : string;
             p : pnode;
         end;
    var beg    : pnode;                                { указатель на начало списка }
        i, key : word;
        s      : string;
        option : word;
    const text: array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
    { -------------- добавление элемента в конец списка --------------------------- }
    procedure add(var beg : pnode; d : word; const s : string);
    var p : pnode;                               { указатель на создаваемый элемент }
        t : pnode;                                 { указатель для просмотра списка }
    begin
        new(p);                                                 { создание элемента }
        p^.d := d; p^.s := s;                                 { заполнение элемента }
        p^.p := nil;
        if beg = nil then beg := p                                { список был пуст }
        else begin                                                 { список не пуст }
            t := beg;
            while t^.p <> nil do                        { проход по списку до конца }
                t := t^.p;            
            t^.p := p;                      { привязка нового элемента к последнему }
        end
    end;
    { ------------------------- поиск элемента по ключу --------------------------- }
    function find(beg : pnode; key : word; var p, pp : pnode) : boolean;
    begin
        p := beg;
        while p <> nil do begin                                                 { 1 }
            if p^.d = key then begin                                            { 2 }
                find := true; exit end;
            pp := p;                                                            { 3 }
            p := p^.p;                                                          { 4 }
        end;
        find := false;
    end;
    
    { -------------------------------- вставка элемента --------------------------- }
    procedure insert(beg : pnode; key, d : word; const s : string);
    var p    : pnode;                            { указатель на создаваемый элемент }
        pkey : pnode;                                { указатель на искомый элемент }
        pp   : pnode;                             { указатель на предыдущий элемент }
    begin
        if not find(beg, key, pkey, pp) then begin
            writeln(' вставка не выполнена'); exit; end;
        new(p);                                                                 { 1 }
        p^.d := d; p^.s := s;                                                   { 2 }
        p^.p := pkey^.p;                                                        { 3 }
        pkey^.p := p;                                                           { 4 }
    end;
    { ------------------------------- удаление элемента --------------------------- }
    procedure del(var beg : pnode; key : word);
    var p  : pnode;         { указатель на удаляемый элемент }
        pp : pnode;         { указатель на предыдущий элемент }
    begin
        if not find(beg, key, p, pp) then begin 
            writeln(' удаление не выполнено'); exit; end; 
        if p = beg then beg := beg^.p                   { удаление первого элемента }
        else pp^.p := p^.p;
        dispose(p);
    end;
    { ------------------------------------ вывод списка --------------------------- }
    procedure print(beg : pnode);
    var p : pnode;                                 { указатель для просмотра списка }
    begin
        p := beg;
        while p <> nil do begin                                    { цикл по списку }
            writeln(p^.d:3, p^.s);                                 { вывод элемента }
            p := p^.p                        { переход к следующему элементу списка }
        end;
    end;
    { ------------------------------- главная программа --------------------------- }
    begin
        for i := 1 to 5 do add(beg, i, text[i]);
        while true do begin 
            writeln('1 - вставка, 2 - удаление, 3 - вывод, 4 - выход');
            readln(option);
            case option of
                1: begin                                                  { вставка }
                    writeln('Ключ для вставки?');
                    readln(key);
                    writeln('Вставляемый элемент?');
                    readln(i); readln(s);
                    insert(beg, key, i, s);
                end;
                2: begin                                                 { удаление }
                    writeln('Ключ для удаления?');   
                    readln(key);
                    del(beg, key);
                end;
                3: begin                                                    { вывод }
                    writeln('Вывод списка:');
                    print(beg);
                end;
                4: exit;                                                    { выход }
            end
            writeln;
        end
    end.

    Функция поиска элемента find возвращает true, если искомый элемент найден, и false в противном случае. Поскольку одного факта отыскания элемента недостаточно, функция также возвращает через список параметров два указателя: на найденный элемент p и на предшествующий ему pp. Последний требуется при удалении элемента из списка, поскольку при этом необходимо связывать предыдущий и последующий по отношению к удаляемому элементы.

    Сначала указатель p устанавливается на начало списка, и организуется цикл просмотра списка (оператор 1). Если поле данных очередного элемента совпало с заданным ключом (оператор 2), формируется признак успешного поиска и функция завершается. В противном случае перед переносом указателя на следующий элемент списка (он хранится в поле p текущего элемента, оператор 4) его значение запоминается в переменной pp (оператор 3) для того, чтобы при следующем проходе цикла в ней находился указатель на предыдущий элемент.

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

    Вставка элемента выполняется после элемента с заданным ключом (процедура insert ). Если с помощью функции find место вставки определить не удалось, выводится сообщение и процедура завершается (в этом случае можно было использовать и другой алгоритм — добавлять элемент к концу списка, это определяется конкретным предназначением программы). Если элемент найден, указатель на него заносится в переменную pkey.

    Под новый элемент выделяется место в динамической памяти (оператор 1), и информационные поля элемента заполняются переданными в процедуру значениями (оператор 2). Новый элемент ).

    (рис 5.4) Вставка элемента в список

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

    Бинарные деревья

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

    Пример бинарного дерева приведен на рис 5.5 (корень обычно изображается сверху). Узел, не имеющий поддеревьев, называется листом. Исходящие узлы называются предками, входящие — потомками. Высота дерева определяется количеством уровней, на которых располагаются его узлы.

    (рис 5.5) Пример бинарного дерева

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

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

    procedure print_tree( дерево );
    begin
        print_tree( левое_поддерево )
        посещение корня
        print_tree( правое_поддерево )
    end;

    Эта процедура позволяет получить последовательность ключей, отсортированную по возрастанию. Результат обхода дерева, изображенного на рис 5.5:

    1, 6, 8, 10, 20, 21, 25, 30

    Если в функции обхода первое обращение идет к правому поддереву, результат обхода будет другим:

    30, 25, 21, 20, 10, 8, 6, 1

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

    Для бинарных деревьев определены операции:

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

    type pnode = ^node;
         node = record
             data  : word;                     { ключ }
             left  : pnode;                    { указатель на левое поддерево }
             right : pnode                     { указатель на правое поддерево }
            end;

    Доступ к дереву в программе осуществляется через указатель на его корень:

    var root : pnode;

    Рассмотрим сначала ).

    function find(root : pnode; key : word; var p, parent : pnode) : boolean;
    begin
        p := root;                             { поиск начинается от корня }
        while p <> nil do begin
            if key = p^.data then              { узел с таким ключом есть }
                begin find := true; exit end;    
            parent := p;                       { запомнить указатель перед спуском }
            if key < p^.data
                then p := p^.left              { спуститься влево }
                else p := p^.right;            { спуститься вправо }
        end;
        find := false;
    end;

    Функция возвращает булевский признак успешности поиска. Ей передаются указатель на корень дерева, в котором выполняется поиск ( root ), и искомый ключ ( key ). Выходными параметрами функции являются указатели на найденный элемент ( p ) и его предка ( parent ).

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

    В приведен пример программы работы с бинарным деревом.

    program bintree;
    uses crt;
    type pnode = ^node;
         node = record
             data  : word;        { ключ }
             left  : pnode;       { указатель на левое поддерево }
             right : pnode        { указатель на правое поддерево }
         end;
    var root   : pnode;
        key    : word;
        option : word;
    { ------------------------------------ вывод дерева --------------------------- }
    procedure print_tree(p : pnode; level : integer);
    var i : integer;
    begin
        if p = nil then exit;
        with p^ do begin
            print_tree(right, level + 1);
            for i := 1 to level do write('     ');
            writeln(data);
            print_tree(left, level + 1);
        end
    end;
    { ------------------------------- поиск по дереву – см. рис. 5.5----------- }
    function find(root : pnode; key : word; var p, parent : pnode) : boolean;
    { ---------------------------- включение в дерево ----------------------------- }
    procedure insert(var root : pnode; key : word);
    var p, parent : pnode;
    begin
        if find(root, key, p, parent) then begin
            writeln(' такой элемент уже есть'); exit; end;
        new(p);                                          { создание нового элемента }
        p^.data  := key;
        p^.left  := nil;
        p^.right := nil;
        if root = nil then root := p                               { первый элемент }
        else                                { присоединение нового элемента к дереву}
            if key < parent^.data
                then parent^.left  := p
                else parent^.right := p;
    end;
    { ------------------------------ удаление из дерева - см. учебник     --------- }
    procedure del(var root : pnode; key : word); 
    { ------------------------------- главная программа --------------------------- }
    begin
        root := nil;
        while true do begin
            writeln('1 - вставка, 2 - удаление, 3 - вывод, 4 - выход');
            readln(option);
            case option of
                1: begin                                                  { вставка }
                        writeln('Введите ключ для вставки: '); readln(key);
                        insert(root, key);
                    end;
                2: begin                                                 { удаление }
                        writeln('Введите ключ для удаления: '); readln(key);
                        del(root, key);
                    end;
                3: begin                                                    { вывод }
                        clrscr;
                        if root = nil then writeln ('дерево пустое')
                        else print_tree(root, 0);
                    end;
                4: exit;                                                    { выход }
            end;
            writeln;
        end
    end.

    Рассмотрим функцию обхода дерева print_tree. Вторым параметром в нее передается целая переменная, определяющая, на каком уровне находится узел. Корень находится на уровне 0. Дерево печатается по горизонтали так, что корень находится слева. Для дерева, изображенного на рис 5.5, вывод выглядит так:

    30    
        25
                21
            20
    10
            8
        6
            1
    Вернуться к учебному плану