Главная страница
Навигация по странице:

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

  • 8.2 Динамические структуры данных 155 8.2.7 Деревья Определим формально дерево

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

  • Контрольные вопросы по главе 8 159

  • 9.1 Модули 163

  • программирование тусур. Программирование учебное пособие. Учебное пособие Томск Эль Контент


    Скачать 0.93 Mb.
    НазваниеУчебное пособие Томск Эль Контент
    Анкорпрограммирование тусур
    Дата17.08.2022
    Размер0.93 Mb.
    Формат файлаpdf
    Имя файлаПрограммирование учебное пособие.pdf
    ТипУчебное пособие
    #647947
    страница14 из 16
    1   ...   8   9   10   11   12   13   14   15   16
    Глава 8. Записи и динамические структуры данных
    Внимательный читатель заметит, что мы допустили серьезную ошибку в опе- рациях (a), (b) и (c). Мы не учли, что список может быть пустым (Ptr=nil). Для этого необходимо ввести очевидные изменения.
    Циклические списки можно использовать не только для представления струк- тур, которым свойственна цикличность, но также для представления линейных структур; циклический список с одним указателем на последний узел, по суще- ству, эквивалентен простому линейному списку с двумя указателями на начало и конец. В связи с этим наблюдением возникает естественный вопрос: Как най-
    ти конец списка, имея в виду круговую симметрию? Пустой связи nil, которая отмечает конец, не существует. Проблема решается так: если мы выполняем неко- торые операции, двигаясь по списку от одного узла к следующему, то мы должны остановиться, когда мы вернулись к исходному месту (предполагая, конечно, что исходное место все еще присутствует в списке).
    Чтобы достичь еще большей гибкости в работе с линейными списками, мы можем включить в каждый узел две связи, указывающие на элементы, находящиеся по обе стороны от данного узла (рис. 8.6). Здесь Left и Right — переменные,
    указывающие на левый и правый концы списка.
    Рис. 8.6 – Список с двумя связями
    В каждом узле списка имеются две связи, назовем их, например, Llink и Rlink. Списки с двумя связями занимают обычно больше пространства памяти,
    чем односвязные. Дополнительные операции, которые можно теперь эффективно выполнять, дают часто более чем достаточную компенсацию за повышенные тре- бования к памяти. Помимо очевидного достоинства, состоящего в том, что при просмотре списков с двумя связями можно продвигаться как вперед, так и назад.
    Еще одним новым важным преимуществом является возможность исключения уз- ла, на который указывает ссылка X, из списка, в котором он находится, если задано только значение X:
    (X

    .Llink)

    .Rlink:=X

    .Rlink;
    (X

    .Rlink)

    .Llink:=X

    .Llink;
    dispose(X);
    Совершенно аналогично в список с двумя связями можно легко включить узел,
    соседний узлу X, как слева, так и справа.
    Операторы new(P); P

    .Rlink:=X

    .Rlink; P

    .Llink:=X;
    (X

    .Rlink)

    .Llink:=P; X

    .Rlink:=P;
    выполняют такое включение справа от узла X; меняя местами левые и правые связи, мы получаем соответствующий алгоритм для включения слева.

    8.2 Динамические структуры данных
    153
    8.2.6 Стеки и очереди
    Очень часто встречаются линейные списки, в которых включение, исключение или доступ к значениям почти всегда производятся в первом или последнем узлах,
    и мы дадим им специальные названия:
    Стек — линейный список, в котором все включения и исключения (и обычно всякий доступ) делаются в одном конце списка (рис. 8.7).
    Рис. 8.7 – Стек
    Очередь — линейный список, в котором все включения производятся на одном конце списка, а все исключения (и обычно всякий доступ) делаются на другом его конце (рис. 8.8).
    Из стека мы всегда исключаем «младший» элемент из имеющихся в списке, т. е.
    тот, который был включен позже других («last-in-first-out» — «последний пришел —
    первый ушел»). Для очереди справедливо в точности противоположное правило:
    исключается всегда самый «старший» элемент; узлы покидают список в том по- рядке, в котором они в него вошли («first-in-first-out» — «первый пришел — первый ушел»).
    Рис. 8.8 – Очередь
    Стеки очень часто встречается на практике. При решении задач наш мозг ведет себя как «стек»: одна проблема приводит к другой, а та в свою очередь к сле- дующей; мы накапливаем в стеке эти задачи и подзадачи и удаляем их по мере того, как они решаются. Аналогично процесс входов в подпрограммы и выходов из них при выполнении машинной программы подобен процессу функционирова- ния стека. Стеки особенно полезны при обработке языков, имеющих архитектуру вложений. К ним относятся языки программирования, арифметические выражения.
    Вообще, стеки чаще всего возникают в связи с алгоритмами, имеющими явно или неявно рекурсивный характер.

    154
    Глава 8. Записи и динамические структуры данных
    Рассмотрим преобразование с помощью стека рекурсивной программы «Ха- нойские башни» к нерекурсивному виду. Напомним исходную программу:
    procedure Tower(i,m,n,p:integer);
    {перекладываем i верхних колец с иглы m на иглу n,
    используя иглу p как вспомогательную}
    begin if i=1 then writeln(‘сделать ход’, m’->‘,n)
    else begin
    Tower(i
    −1,m,p,n);
    writeln(‘сделать ход’, m’->‘,n);
    Tower(i
    −1,p,n,m)
    end end;
    Видно, что задача «переложить i верхних дисков с иглы m на иглу n» сводится к трем задачам того же типа: двум задачам с i
    − 1 дисками и к одной задаче с единственным диском. Занимаясь этими задачами, важно не позабыть, что еще осталось делать. Для этой цели заведем стек со следующим типом элементов type solve = record i,m,n,p:integer end;
    Каждая такая запись интерпретируется как заказ «переложить i верх- них диска с иглы m на иглу n, используя иглу p как вспомогательную». Заказы упо- рядочены в соответствии с требуемым порядком выполнения: самый срочный —
    вершина стека. Стек можно реализовать с помощью линейного списка.
    Получаем такую программу:
    procedure Tower(i,m,n,p:integer);
    {перекладываем i верхних колец с иглы m на иглу n,
    используя иглу p как вспомогательную}
    begin
    {сделать стек заказов пустым}
    {положить в стек запись }
    while {стек не пуст} do begin
    {удалить верхний элемент, переложив его в
    if j=1 then writeln(‘сделать ход’, x, ’->‘,y)
    else begin
    {положить в стек записи:

    <1,x,y,z>
    }
    end end end;
    Заметим, что первой в стек кладется запись, которую надо выполнять последней.

    8.2 Динамические структуры данных
    155
    8.2.7 Деревья
    Определим формально дерево как конечное множество T, состоя-
    щее из одного или более узлов, удовлетворяющих следующим свой-
    ствам.
    Имеется один специальный узел, называемый корнем дан-
    ного дерева.
    Остальные узлы (исключая корень) содержатся в m
    ⩾ 0
    попарно не пересекающихся множествах T
    1
    ,. . ., T
    m
    , каж-
    дое из которых в свою очередь является деревом. Деревья
    T
    1
    ,. . ., T
    m
    называются поддеревьями данного дерева.
    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
    Из нашего определения следует, что каждый узел дерева является корнем неко- торого поддерева, которое содержится в этом дереве. Число поддеревьев данного узла называется степенью этого узла. Узел с нулевой степенью называется кон-
    цевым узлом, или листом. Дерево называется бинарным, если каждый узел имеет степень не больше двух.
    Но обычно бинарное дерево определяется несколько иначе. Например, рас- смотрим бинарные деревья с базовым типом integer.
    Бинарное дерево либо пусто, либо состоит из узла, содержащего
    целое число, и левого и правого поддеревьев, являющихся бинарны-
    ми деревьями.
    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
    Соответствующее определение типа бинарное дерево, использующее ссылки,
    следующее:
    type tree =

    node; {ссылка на узел дерева}
    node = record {узел дерева}
    info:integer;
    left,right:tree end
    Пустое дерево обычно обозначается ссылкой nil. На рис. 8.9 изображено одно из бинарных деревьев. Узел с числом 7 является корнем дерева. Дерево обычно изображают корнем вверх. Узлы, имеющие только пустые поддеревья, называются
    листьями (в данном дереве это узлы: 15, 2, 100 и 10). Пустые поддеревья листьев не показаны.
    Каждый узел поддерева имеет свой уровень, который определяется рекурсивно:
    • уровень корня бинарного дерева равен 0;
    • если уровень узла равен n, то уровни корней левого и правого поддеревьев равны n+1.

    156
    Глава 8. Записи и динамические структуры данных
    Рис. 8.9 – Пример бинарного дерева
    Обход дерева
    До конца раздела будем рассматривать только бинарные деревья, поэтому при- лагательное бинарное будем опускать.
    Имеется много задач, которые можно выполнять на древовидной структуре:
    распространенная задача — выполнение заданной операции P с каждым элементом
    дерева. Здесь P рассматривается как параметр более общей задачи посещения всех узлов, или, как это обычно называют, обхода дерева.
    Если рассматривать эту задачу как единый последовательный процесс, то от- дельные узлы посещаются в некотором определенном порядке и могут считаться расположенными линейно. В самом деле, описание многих алгоритмов существен- но упрощается, если можно говорить о переходе к следующему элементу дерева,
    имея в виду некоторое упорядочение.
    Существуют три принципа упорядочения, которые естественно вытекают из структуры деревьев. Так же как и саму древовидную структуру, их удобно выразить с помощью рекурсии. Пусть R обозначает корень, а A и B левое и правое поддеревья,
    тогда можно определить такие три упорядочения:
    • сверху вниз: R, A, B (посетить корень до поддеревьев);
    • слева направо: A, R, B;
    • снизу вверх: A, B, R (посетить корень после поддеревьев).
    Обходя дерево из рис. 8.9 и выписывая числа, находящиеся в узлах, в том порядке, в котором они встречаются, мы получаем следующие последовательности:
    • сверху вниз: 7, 9, 15, 27, 2, 13, 3, 100, 10;
    • слева направо: 15, 9, 2, 27, 7, 13, 100, 3, 10;
    • снизу вверх: 15, 2, 27, 9, 100, 10, 3, 13, 7.
    Теперь выразим эти три метода обхода как три конкретные программы с явным параметром t, означающим дерево, с которым они имеют дело, и неявным парамет-

    8.2 Динамические структуры данных
    157
    ром P, означающим операцию, которую нужно выполнить с каждым узлом. Эти три метода легко сформулировать в виде рекурсивных процедур; они вновь служат примером того, что действия с рекурсивно определенными структурами данных лучше всего описываются рекурсивными алгоритмами.
    procedure preorder(t:tree);
    begin if t <> nil then begin P(t); preorder(t

    .left);
    preorder(t

    .right) end end;
    procedure postorder(t:tree);
    begin if t<>nil then begin postorder(t

    .left);
    postorder(t

    .right); P(t) end end;
    procedure inorder(t:tree);
    begin if t<>nil then begin inorder(t

    .left); P(t);
    inorder(t

    .right) end end;
    Поиск по дереву с включением
    Бинарные деревья часто используются для представления множества данных,
    элементы которых ищутся по уникальному, только им присущему ключу. Если дерево организовано таким образом, что для каждого узла t все ключи в левом поддереве меньше ключа t, а ключи в правом поддереве больше ключа t, то это дерево называется деревом поиска. В дереве поиска можно найти место каждо- го ключа, двигаясь, начиная с корня и переходя на левое или правое поддерево каждого узла в зависимости от значения его ключа.
    Возможности техники динамического размещения переменных с доступом к ним через ссылки демонстрируются в задачах, в которых сама структура дерева изменяется, т. е. дерево растет и/или уменьшается во время выполнения программы.
    Хорошим примером этого является задача построения частотного словаря.
    В этой задаче задана последовательность слов, и нужно установить число появ- лений каждого слова. Это означает, что, начиная с пустого дерева, каждое слово ищется в дереве. Если оно найдено, увеличивается его счетчик появлений, если нет — в дерево вставляется новое слово (с начальным значением счетчика, рав- ным 1).
    Для простоты будем использовать целые числа в качестве слов.
    Предлагаются следующие описания типов:
    type tree =

    word;
    word = record key:integer;
    count:integer;

    158
    Глава 8. Записи и динамические структуры данных
    left,right:tree end;
    Пусть у нас есть исходный файл ключей f, а переменная root указывает на корень дерева поиска, мы можем записать программу следующим образом:
    root:=nil;
    reset(f);
    while not eof(f) do begin read(f,x); search(x,root) end
    Процесс поиска с включением представлен в виде рекурсивной процедуры search
    :
    procedure search(x:integer;var p:tree);
    begin if p=nil then begin {слова нет в дереве;включить его}
    new(p);
    with p

    do begin key:=x;count:=1;left:=nil;right:=nil end end else if x

    .key then search(x,p

    .left) else if x>p

    .key then search(x,p

    .right)
    else p

    .count:=p

    .count+1
    end
    Для входной последовательности 8, 9, 11, 15, 7, 3, 2, 1, 5, 6, 4, 10 программа строит бинарное дерево поиска (рис. 8.10).
    Рис. 8.10 – Упорядоченное бинарное дерево

    Контрольные вопросы по главе 8
    159
    Сортировка деревом
    Хотя задача предыдущего алгоритма — поиск, его можно применить и для сор- тировки. Действительно, после того как входной файл ключей запишется в дерево поиска, мы можем получить отсортированную последовательность, обходя дерево методом слева направо и печатая в данном порядке ключи:
    procedure print(t:tree);
    begin if t<>nil then begin print(t

    .left); write(t

    .key:5);
    print(t

    .right) end end
    Вызов этой процедуры с параметром, равным корню построенного дерева,
    выдаст отсортированную последовательность 1, 2, 3, 4, 5, 6, 7, 8, 9 10, 11, 15.
    Эта сортировка так же быстра, как быстрая сортировка Хоара. Кроме того, данную сортировку удобно применять для сортировки файлов: читаем последовательность элементов из файла и создаем упорядоченное бинарное дерево, потом обходим его и записываем элементы в новый файл.
    Контрольные вопросы по главе 8 1. Описать тип записи для представления следующего понятия:
    а) время в часах, минутах и секундах;
    б) данные из медицинской карты (фамилия, имя, отчество, пол, возраст);
    в) адрес (город, улица, дом, квартира);
    г) бланк требования на книгу в библиотеке (сведения о книге: шифр,
    автор, название; сведения о читателе: номер читательского билета,
    фамилия; дата заказа).
    2. Ответьте на следующие вопросы:
    а) Верно ли, что все поля записи должны быть разных типов?
    б) Почему при описании записи её поля могут перечисляться в любом порядке?
    в) Верно ли, что названия полей записи могут совпадать с именами пе- ременных, констант и других объектов программы, но не могут сов- падать с названиями полей других записей?
    г) Почему в переменной-поле (т. е. конструкции r.f) имя поля (f ) долж- но указываться явно и не может быть задано в виде выражения?
    3. type ref =

    integer;
    var p,q: ref;

    160
    Глава 8. Записи и динамические структуры данных
    Пусть переменные p и q получили значения после выполнения следующих операторов:
    new(p); new(q); p

    := 5; q

    := 3;
    Ответьте на следующие вопросы:
    а) Что является значением переменной p: ссылка на объект (переменную)
    целого типа или сам этот объект? Что обозначает переменная p

    : ссыл- ку на объект целого типа, сам этот объект или целое 5? Каковы типы переменных p и p

    ?
    б) Что будет выдано на печать в результате выполнения следующих опе- раторов?
    p

    := q

    ;
    if p = q the p = nil else if p

    = q

    then q := p;
    if p = q then q

    := 4;
    writeln(p

    );
    4. Почему объекты (переменные), создаваемые процедурой new и уничтожае- мые процедурой dispose, называют динамическими? Почему им не дают имена?
    5. Имеется описание var x:

    boolean; y: booolean;
    Можно ли переменной x присвоить ссылку на переменную y? Можно ли с помощью процедуры dispose уничтожить переменные x и y?
    Рекомендуемая литература к главе 8
    [1]
    Кнут Д. Искусство программирования / Д. Кнут. — 3-е изд. — М. : Вильямс,
    2005. — Т. 1 : Основные алгоритмы. — 712 с.
    [2]
    Фаронов В. В. Турбо Паскаль 7.0: Практика программирования / В. В. Фа- ронов. — М. : Нолидж, 2000. — 416 с.
    [3]
    Вирт Н. Алгоритмы + структуры данных = программы / Н. Вирт. — М. :
    Мир, 1985. — 405 с.

    Глава 9
    МОДУЛИ И ГРАФИКА
    9.1 Модули
    9.1.1 Модульное программирование
    Понятие модуля или, в общем случае, модульного программирования возникло на определенном этапе развития программирования и было обусловлено, в первую очередь, возрастающими объемами программ, их увеличивающейся внутренней сложностью и коллективным характером разработок. Модули — независимо храни- мые и разрабатываемые, независимо компилируемые и тестируемые программные единицы со строго определенными интерфейсами, которые могут объединяться с главной программой.
    Модуль — совокупность программных ресурсов (констант, типов, переменных,
    подпрограмм), предназначенных для использования другими модулями и программами.
    Сам модуль не выполняется, используются только его объекты.
    Ресурсы модуля делятся на две части:
    Интерфейс — то, что используется в других модулях и программах. Сюда входит описание объектов, доступных (видимых) из других программ.
    Реализация — то, что используется только в данном модуле. Эта часть со- держит описание объектов, недоступных (невидимых, скрытых) от других программ.
    Модуль в общем виде имеет следующее представление.
    unit Unitname;
    {имя модуля — произвольный идентификатор; этот модуль должен храниться в файле с тем же именем, в данном случае, в файле с именем unitname.pas}
    interface {описание видимых объектов}
    implementation {описание скрытых объектов}
    begin

    162
    Глава 9. Модули и графика
    {здесь могут присутствовать операторы, инициализирующие объекты модуля; установка значений переменных}
    end.
    Следующий пример модуля содержит только интерфейсную часть.
    unit Calendar;
    interface type
    Days = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
    WorkingDays = Mon..Fri;
    Months = (jan, feb, mar, apr, may, jun, jul, aug, sep,
    oct, nov, dec);
    Summer = jun..aug;
    Autumn = sep..nov;
    Spring = mar..may;
    DayNo = 1..31;
    YearNo = 1900..2000;
    Date = record Day: DayNo; Month : Months;
    Year : YearNo; end;
    implementation begin end.
    Если какая-то функция из модуля будет использоваться в других модулях или программах, то ее определение разбивается на две части. В интерфейс модуля помещается заголовок функции, а в реализацию — ее тело.
    Следующий пример модуля реализует действия над комплексными числами.
    unit Comp;
    interface type
    Complex = record Re, Im: real;end;
    procedure InitC (var C: Complex; R, I: real);
    {создание комплексного числа}
    procedure AddC (C1, C2: Complex; var C3: Complex);
    {сложение целых чисел}
    implementation procedure InitC (var C: Complex; R, I: real);
    begin with C do begin Re:=R; Im:=I end end;
    procedure AddC (C1, C2: Complex; var C3: Complex);
    begin with C3 do begin
    Re := C1.Re + C2.Re; Im := C1.Im + C2.Im end;
    end;

    9.1 Модули
    163
    begin end.
    Модификация реализации при прежнем интерфейсе никак не отражается на программах, использующих модули.
    Прежде чем модуль можно использовать в программах, его необходимо отком- пилировать. Если модуль содержится в файле name.pas, то в результате компи- ляции образуется дисковый файл name.tpu.
    Если программа использует объекты из модулей u1, u2, u3, то первой строкой в программе (после возможного заголовка программы) должна быть директива для компилятора uses u1, u2,u3;
    Если модуль использует другие модули, то это задается с помощью такой же директивы.
    unit u;
    interface uses u1,u2,u3;
    Пример uses Comp;
    var
    C1,C2,C3: Complex;
    begin
    InitC(C1,1,2); InitC(C2,3,
    −5);
    AddC(C1,C2,C3);
    end.
    Наиболее часто используемые модули (так называемые стандартные модули)
    хранятся в библиотеке — файле turbo.tpl. С помощью служебной программы
    Tpumover пользователь может включить в библиотеку и какой-то собственный модуль.
    Приведем пример полезного модуля, реализующего операции со стеком.
    unit StackOps; {операции над стеком целых чисел}
    interface procedure Push (Elem: integer);
    {добавление элемента в стек}
    function Pop: integer; {извлечение элемента из стека}
    function Empty: boolean; {проверка стека на пустоту}
    function Full: boolean; {проверка стека на переполнение}
    implementation
    {Стек реализован в виде линейного массива целых элементов. Переменная Top отмечает позицию стека над вершиной.}

    164
    1   ...   8   9   10   11   12   13   14   15   16


    написать администратору сайта