Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
31
Добавлен:
23.03.2015
Размер:
88.06 Кб
Скачать

Практична робота №3

Техніка роботи з пов’язаними структурами даних.

Найпростіший спосіб пов'язати декілька елементів - це розташувати їх лінійно в списку. В цьому випадку кожен елемент містить тільки одне посилання що пов'язує його з наступним елементом списку.

Хай тип T описаний таким чином:

type T = record

Key: integer; { Ключове поле, що однозначно ідентифікує елемент }

Info: infotype; { Інформація, що зберігається у вузлі }

Next: ^T { Поле зв'язку, яке вказує на наступний елемент }

end;

Список елементів типу T можна зобразити таким чином:

╔═════════════════════════════════════════════════════════╗

║ Дескриптор списку REP (Представлення списку) ║

║ ┌────────────────────┐ ║

PV962───╫─>│ First ───────────┼─┐ ║

║ ├────────────────────┤ │ ║

║ │ Count = 3 │ │ ║

║ └────────────────────┘ │ ║

║ ┌──────────────┘ ║

║ ┌───────┼───────────────────────────────────────────┐ ║

║ │ Тіло списку (Набір взаємозв’язаних вузлів) │ ║

║ │ ┌─────V─────┐ │ ║

║ │ │ 1 │ │ ║

║ │ ├───────────┤ │ ║

║ │ │ Иванов │ │ ║

║ │ ├───────────┤ ┌───────────┐ │ ║

║ │ │ Next ─────┼───>│ 2 │ │ ║

║ │ └───────────┘ ├───────────┤ │ ║

║ │ │ Петров │ │ ║

║ │ ├───────────┤ ┌───────────┐ │ ║

║ │ │ Next ─────┼───>│ 3 │ │ ║

║ │ └───────────┘ ├───────────┤ │ ║

║ │ │ Сидоров │ │ ║

║ │ ├───────────┤ │ ║

║ │ │ Nil │ │ ║

║ │ └───────────┘ │ ║

║ └───────────────────────────────────────────────────┘ ║

╚════════════════════════════════════════════════════════╝

Тут показаний конкретний екземпляр списку, що складається з трьох елементів. У якості інформаційного поля узято прізвище студента, а ключове поле - номерів студента в журналі. Представлення списку (Reprezentation) складається з дескриптора і тіла. Дескриптор структури зберігає загальні для всієї структури поля, а тіло структури зберігає набір взаємозв'язаних елементів структури. Зв'язки можуть бути явними як в нашому випадку (поле Next) або неявними (наприклад, сусідні елементи структури можуть займати сусідні елементи пам'яті). Змінна Pv962 - це зовнішня змінна, яка зберігає посилання на екземпляр списку.

Закінчимо опис наший структури:

type

infotype = string [20]; { Прізвище студента }

groupptr = ^group; { Покажчик на групу }

group = record

First: ^T; { Покажчик на перший вузол списку }

Count: byte; { Кількість студентів в групі }

end;

Конкретний екземпляр структури оголошується в розділі var:

var

PV962: groupptr;

Але таке оголошення не створює список студентів групи, а тільки резервує місце для покажчика. Створення структури здійснюється з допомогою спеціальної операції-конструктора. Назвемо цю операцію Creategroup. Сенс цій операції визначається наступною специфікацією:

function CreateGroup: groupptr;

{ Effects: Вводить список групи з клавіатури і створює екземпляр }

{ списку групи в пам'яті. Заповнює поля дескриптора First }

{ і Count }

У структурі можуть бути і інші конструктори. Групу можна створювати, наприклад

на основі інформації, що зберігається в масиві.

Для динамічних структур, що створюються в області купи, єдиним способом їх створення є метод поелементної вставки. Простим способом включення в лінійний список є вставка в початок. Реалізація цієї операції може бути виконана таким чином:

procedure InsertFirst(var G: group; Numb: integer; Fio: infotype);

{ Effects: Додає студента з номером Numb і прізвищем Fio в початок }

{ списку групи G }

var

q: ^T;

begin

new(q); { Виділити пам'ять в купі під вузол списку }

q^.Key:=Numb; q^.Info:=Fio; { Заповнити його інформацією }

q^.Next:=G.First; { Вставити його в початок списку групи G }

inc(G.Count); G.First:=q; { Модифікувати дескриптор }

end;

Тепер можна реалізувати і сам конструктор CreateGroup.

function CreateGroup: groupptr;

{ Effects: Вводить список групи з клавіатури в зворотному порядку і }

{ створює екземпляр списку групи в пам'яті

. }

var

p: ^T; finish: boolean;

g: groupptr;

begin

new(g);

g^.First:=nil; { Почнемо побудову }

g^.Count:=0; { з порожнього списку }

finish:=false; { Ознака завершення роботи }

writeln('Создание списка группы методом вставки в начало:');

writeln(' Студенты должны вводиться в обратном порядке!');

writeln(' Признак конца ввода - студент с номером 0 ');

writeln;

while not finish do begin

writeln('Введите очередного студента: Numb и Fio');

readln(Numb,Fio);

if Numb=0 then

finish:=true

else

InsertFirst(g, Numb, Fio);

end;

end;

Звернете увагу, що вводити студентів потрібно в порядку, зворотному початковому оскільки при створенні списку методом вставки в початок список виходить інвертованим. Щоб створений в пам'яті список був правильним його і потрібно було вводити в зворотному порядку. Цей факт знайшов своє віддзеркалення і в специфікації операції.

Такий спосіб створення не завжди зручний. Отже, потрібно уміти створювати лінійні списки методом вставки в кінець. Це можна зробити наступним чином:

procedure InsertLast(var G: group; Numb: integer; Fio: infotype);

{ Effects: Додає студента з номером Numb і прізвищем Fio в кінець

{ списку групи G }

var

p,q: ^T;

begin

new(q); { Виділити пам'ять в купі під вузол списку }

q^.Key:=Numb; q^.Info:=Fio; { Заповнити його інформацією }

q^.next:=nil; { Відзначити, що він тепер останній }

if G.First=nil then { Вставка в порожній список }

begin

q^.Next:=G.First; { Вставити його в початок списку групи G }

inc(G.Count); G.First:=q; { Модифікувати дескриптор }

end else { Вставка в непорожній список }

begin

p:=G.First; { Пошук }

while p^.next <> nil do { останнього вузла }

p:=p^.next; { списку }

p^.next:=q; { Вставити в кінець }

end;

end;

Як видимий цей метод володіє поряд недоліків. А саме:

1. Необхідно при кожній вставці пробігати список від початку до кінця при пошуку останнього вузла списку.

2. Обробка випадку вставки в порожній список відбувається інакше.

Перший недолік можна усунути додавши в дескриптор списку покажчик на

останній вузол списку:

group = record

First: ^T; { Покажчик на перший вузол списку }

Last : ^T; { Покажчик на останній вузол списку }

Count: byte; { Кількість студентів в групі }

end;

Тепер реалізація операції виглядає простішою:

procedure InsertLast(var G: group; Numb: integer; Fio: infotype);

{ Effects: Додає студента з номером Numb і прізвищем Fio в кінець

{ списку групи G }

var

p,q: ^T;

begin

new(q); { Виділити пам'ять в купі під вузол списку }

q^.Key:=Numb; q^.Info:=Fio; { Заповнити його інформацією }

q^.next:=nil; { Відзначити, що він тепер останній }

if G.First=nil then { Вставка в порожній список }

begin

q^.Next:=G.First; { Вставити його в початок списку групи G }

inc(G.Count); G.First:=q;

G.Last:=q; { Модифікувати дескриптор }

end else { Вставка в непорожній список }

begin

G.Last^.next:=q; { Вставити в кінець }

G.Last:=q { Модифікувати дескриптор }

end;

end;

Звернете увагу, що поля дескриптора в структурі повинні мінятися при зміненні стану структури.

Одними з найбільш типових операцій над списками є операції включення і видалення елементів списку, а також операція перегляду списку.

Розглянемо спочатку операцію включення.

Припустимо, що елемент, на який указує посилання q, необхідно включити після елементу, на який указує посилання р. Hеобхідні привласнення значень посиланням повинні виглядати таким чином:

q^.next:=p^.next; p^.next:=q;

Зверніть увагу, що ці привласнення необхідно виконувати саме в такому порядку. Цю операцію можна проілюструвати наступним малюнком:

┌──────┐ ┌──────┐

│ │ ┌─>│ │

├──────┤ │ ├──────┤

q──>│ │ │ │ │

├──────┤ │ ├──────┤

│ │ │ │ *───┼─┐

└──────┘ │ └──────┘ │

└──────┐ ┌──┘

┌──────┐ ┌──────┐ ┌──────┐ │ │ ┌──────┐

┌─────>│ │ ┌──>│ │ ┌──> ┌─────>│ │ │ └>│ │ ┌──>

│ ├──────┤ │ ├──────┤ │ │ ├──────┤ │ ├──────┤ │

│ p──>│ │ │ │ │ │ │ p──>│ │ │ │ │ │

│ ├──────┤ │ ├──────┤ │ │ ├──────┤ │ ├──────┤ │

─┘ │ *───┼──┘ │ *────┼─┘ ─┘ │ *───┼──┘ │ *────┼─┘

└──────┘ └──────┘ └──────┘ └──────┘

Рис. 1. Включення вузла q після вузла р.

Якщо потрібне включення нового вузла q перед вузлом, на який указує p то здається, що це зробити складніше, оскільки немає доступу до вузлів передуванням даному. Проте простій "трюк" дозволяє вирішити цю проблему.

Цей трюк показаний на Рис. 2.

Стан фрагмента списку до вставки вузла q.

┌──────┐

│ 8 │

├──────┤

q──>│ │

├──────┤

│ │

└──────┘

┌──────┐ ┌──────┐ ┌──────┐

┌──>│ 13 │ ┌─────>│ 27 │ ┌──>│ 21 │ ┌──>

│ ├──────┤ │ ├──────┤ │ ├──────┤ │

│ │ │ │ p──>│ │ │ │ │ │

│ ├──────┤ │ ├──────┤ │ ├──────┤ │

──┘ │ *────┼─┘ │ *───┼─┘ │ *────┼─┘

└──────┘ └──────┘ └──────┘

Стан фрагмента списку після вставки вузла q.

┌──────┐

┌─────>│ 27 │

│ ├──────┤

│ q──>│ │

│ ├──────┤

│ │ *───┼──────────┐

│ └──────┘ │

└───────────────────────┐│

┌──────┐ ┌──────┐ ││ ┌──────┐

┌──>│ 13 │ ┌─────>│ 8 │ │└─>│ 21 │ ┌──>

│ ├──────┤ │ ├──────┤ │ ├──────┤ │

│ │ │ │ p──>│ │ │ │ │ │

│ ├──────┤ │ ├──────┤ │ ├──────┤ │

──┘ │ *────┼─┘ │ *───┼─┘ │ *────┼─┘

└──────┘ └──────┘ └──────┘

Рис. 2. Включення вузла q перед вузлом р.

Трюк полягає в тому, що нова компоненту насправді вставляється після вузла p, але потім відбувайся обмін значеннями між новим вузлом (q^) і вузлом, перед яким необхідно виконувати вставку (p^). Це можна робити на підставі того, що "список - це структура з довільним розміщенням і послідовною обробкою". Т. е. для списку важливий логічний порядок проходження елементів, а не фізичні адреси їх розміщення. Рішення може бути отримано за допомогою наступного фрагмента програми:

new(q); q^:=p^; p^.key:=k; p^.next:=q;

Тут k - це ключ елементу, що вставляється.

Тепер розглянемо процес видалення із списку. Видалення елементу наступного за вузлом p очевидно і може бути виконано наступним фрагментом програми:

r:=p^.next; p^.next:=r^.next; r^.next:=q; q:=r;

Тут видалення з одного списку показане в комбінації з додаванням елементу, що видаляється, в інший список після вузла q. Цю операцію ілюструє Рис. 3.

Стан фрагмента списку до видалення вузла, розташованого після р.

┌──────┐ ┌──────┐

┌─────>│ │ ┌──>│ │ ┌──>

│ ├──────┤ │ ├──────┤ │

│ q──>│ │ │ │ │ │

│ ├──────┤ │ ├──────┤ │

─┘ │ *───┼──┘ │ *────┼─┘

└──────┘ └──────┘

┌──────┐ ┌──────┐ ┌──────┐

┌─────>│ │ ┌──>│ │ ┌──>│ │ ┌──>

│ ├──────┤ │ ├──────┤ │ ├──────┤ │

│ p──>│ │ │ │ │ │ │ │ │

│ ├──────┤ │ ├──────┤ │ ├──────┤ │

│ *───┼──┘ │ *────┼─┘ │ *────┼─┘

└──────┘ └──────┘ └──────┘

Стан фрагмента списку після видалення вузла, розташованого після p і додавання його в інший список після вузла, розташованого після q.

┌──────┐ ┌──────┐

┌─────>│ │ ┌>│ │ ┌──>

│ ├──────┤ │ ├──────┤ │

│ q──>│ │ │ │ │ │

│ ├──────┤ │ ├──────┤ │

─┘ │ *───┼─┐ │ │ *────┼─┘

└──────┘ │ │ └──────┘

┌─────┘ └──┐

┌──────┐ │ ┌──────┐ │ ┌──────┐

┌─────>│ │ └─>│ │ │ ┌>│ │ ┌──>

│ ├──────┤ ├──────┤ │ │ ├──────┤ │

│ p──>│ │ │ │ │ │ │ │ │

│ ├──────┤ ├──────┤ │ │ ├──────┤ │

─┘ │ *───┼──┐ │ *────┼─┘ │ │ *────┼─┘

└──────┘ │ └──────┘ │ └──────┘

└──────────────┘

Рис. 3. Видалення із списку і включення в інший список.

При роботі із списками малюнки дуже корисні і допомагають уникнути прикрих

помилок. Важливо проставити на малюнку порядок виконання змін посилань, т.

до. порушення правильного порядку може привести до втрати посилань на ще не

прив'язані вузли.

Важче видалити сам вказаний елемент (а не наступний за ним) оскільки ми стикаємося з тією ж проблемою, що і при включенні перед p: повернення до елементу, який передує вказаному, неможливе. Але, можливо, видалити подальший елемент, заздалегідь переславши його значення ближче до початку списку. Це задоволено простій і очевидний прийом, але його можна застосувати тільки у тому випадку, коли у вузла p є подальший елемент, тобто він не є останнім в списку.

Тепер розглянемо основну операцію, використовувану при обробці списків, а саме операцію проходу за списком (ітерацію по вузлах списку).

Припустимо, що операція P(x) повинна виконуватися з кожним елементом списку.

Це завдання можна виконати таким чином:

cur:=L.First;

while cur<>nil do begin

P(cur^);

cur:=cur^.next;

end;

Дуже часто використовуваною операцією для лінійних списків є пошук у списку елементу із заданим ключем k0. Пошук ведеться строго послідовно.

Пошук може закінчитися по двох причинах: або, коли елемент знайдений, або коли досягнутий кінець списку. Перша спроба рішення цієї задачі може привести нас до наступного фрагмента програми:

cur:=L.First;

while (p<>nil) and (cur^.key<>k0) do cur:=cur^.next;

Проте уважний аналіз цього фрагмента виявляє наступну помилку: при

cur=nil не існує і cur^. Отже, обчислення умови закінчення може зажадати звернення до неіснуючої змінної, що може привести до аварійного завершення програми. Це можна виправити введенням логічної змінній для індикації умови виходу з циклу. Ми приходь до наступної версії програми:

var Found: boolean;

...

Finish:=false;

cur:=L.First;

while not Finish do

if cur<>nil then

if cur^.Key = k0 then Finish:=true

else cur:=cur^.next

else Finish:=true;

Такий цикл може закінчитися по двох причинах:

1) шуканий елемент знайдений;

2) список закінчився і означає шуканого елементу в списку немає (невдалий

пошук).

Тому після такого циклу необхідно аналізувати результат пошуку. Це

можна зробити таким чином:

if cur = nil then << невдалий пошук >>

else << вдалий пошук >>

Соседние файлы в папке Линейные структуры