
Struktura_danikh_Ch1
.pdf
При цьому порушується правило, згідно з яким кожний об’єкт повинен бути описаний до його використання. Але оскільки у описі присутня рекурсія (stack використовує selem, а selem - stack), дотриматись цього правила неможливо. Тому у Паскалі робиться виключення для типів вказівників. А саме: тип вказівника може бути описаний раніше, ніж тип змінної, на яку він вказує, але останній повинен бути описаний у тому ж розділі описів, що й тип вказівника.
Почати роботу зі стеком (процедура Init) – це просто присвоїти вказівнику на верхівку стеку значення nil.
Перевірити, чи стек порожній (функція Empty), означає порівняти вказівник на вершину стеку з nil.
Зміни стеку під час дії „вштовхнути елемент” (процедура Push) зображені на рис. 3.6. Після new(p) (рис. 3.6 а) ) виділяється пам’ять під новий елемент стеку. Ланцюг p^.d:=c; p^.next:=s (рис. 3.6 б) забезпечує запис символу c у поле даних та зв’язування нового елемента з поточною верхівкою стеку. Запис p^ дає змінну, на яку вказує вказівник. Ця змінна є записом, отже p^.d та p^.next – це відповідні поля цього запису. Операція s:=p змінює вказівник на верхівку стеку (рис. 3.6 в). Пунктирна стрілка тут і далі позначає розірваний зв’язок.
Функція Top перевіряє, чи порожній стек, і якщо так, то видає повідомлення про помилку та аварійно завершує роботу всієї програми викликом стандартної підпрограми halt. Якщо стек не порожній, то функція повертає символ, що зберігається у полі даних верхнього елемента стеку.
Рисунок 3.6 - Додавання елемента у стек
Зміни стеку під час дії „забрати верхівку стеку” (процедура Pop) зображені на рис. 3.7. Після операції p:=s необхідно запам’ятати вказівник на верхівку стека у змінній p (рис. 3.7 а). Операція s:=s^.next змінює вказівник на верхівку стеку (рис. 3.7 б). Команда dispose(p) звільняє пам’ять, яку займав колишній верхній елемент стеку (рис. 3.7 в).
33

Рисунок 3.7 - Видалення верхівки стеку
Модуль стеку символів може бути використаний для розв’язку такої задачі. Вводиться послідовність символів, яка завершується символом ‘#’. Треба показати цю послідовність у зворотньому порядку.
У програмі Inversion послідовність символів, що введена, записується у стек. Потім символи забираються зі стеку та виводяться на екран у зворотньому порядку.
{ Запис послідовності символів у зворотньому порядку } program Inversion;
uses ChStack; const EndCh = '#';
var s: stack; c : char; begin
Init(s);
writeln('Введіть послідовність символів. Кінец введення - ',EndCh); read(c);
while c <> EndCh do begin Push(s,c); read(c)
end;
writeln('Послідовність у зворотньому порядку'); while not Empty(s) do begin
write(Top(s));
Pop(s)
end;
writeln;
end.
34

Модуль ChStack та програма Inversion демонструють стандартний підхід до використання вказівників. Він обмежений модулем, а програма тільки викликає підпрограми, що реалізують дії над стеком. Обмеження використання вказівників зумовлене небезпекою, пов’язаною з безпосередньою роботою з пам’яттю. Так ланцюг
new(p2); … p1:=p2; dispose(p2)
звільняє блок пам’яті, на який вказував p2, і він може бути використаний для розміщення інших даних. В той же час, можливе подальше використання вказівника p1 для посилання на вже звільнену (і, можливо, перерозподілену) пам’ять. Така ситуація є дуже небезпечною та може привести навіть до виведення з ладу операційної системи.
3.3 Черга
Черга або одностороння черга – це лінійний список, в якому всі операції вставлення виконуються на одному з кінців списку, а всі операції видалення (і, як правило, операції доступу до даних) – на іншому.
Черга – ще одна рекурсивна структура даних, яку можна визначити так:
1Порожня черга.
2Перший елемент; черга.
Чергу можна представити, як сукупність однотипних елементів, в якій користувач має доступ до кінця черги при додаванні елементів та до початку черги при взятті елементів (рис. 3.8).
Рисунок 3.8 – Черга
Операції, відношення та інструкції для черг:
1Почати роботу.
2Чи порожня черга?
3Додати елемент до кінця черги.
4Взяти елемент з початку черги.
Дії 1, 3, 4 – інструкції; 2 – відношення.
“Почати роботу” означає створити порожню чергу.
“Додати елемент до кінця черги” – додати до черги один елемент, який стає останнім у черзі.
“Взяти елемент” – взяти та повернути значення першого елемента. Першим стає наступний елемент черги або черга стає порожньою. Для порожньої черги ця інструкція повинна давати відмову.
35

Можлива реалізація черги з використанням вказівників зображена на рис. 3.9. Елементи черги – це записи з двох полів: поле даних та вказівник на наступний елемент. За допомогою вказівника елементи черги з’єднані у ланцюг. У останньому елементі вказівник на наступний елемент дорівнює nil. Саму чергу реалізовано, як запис з двох вказівників: на початок черги та на кінець черги. Вказівник на кінець черги потрібний для того, щоб при додаванні елементів не проходити всю чергу від початку до кінця. Якщо черга порожня, то елементів немає, а обидва вказівники дорівнюють nil.
Рисунок 3.9 - Реалізація черги
Реалізувати чергу цілих чисел мовою Паскаль можна за допомогою модуля IntQueue. Тут qref – вказівник на елементи черги; qelem – елемент черги; queue – черга; поле bg – вказівник на початок черги, а en – вказівник на кінець черги.
unit IntQueue; |
|
|
interface |
|
|
type qref = ^qelem; |
{ Вказівник на елемент черги } |
|
qelem = record |
{ Елемент черги } |
|
d: integer; |
|
|
next: qref |
|
|
end; |
|
|
queue = record |
{ Черга } |
|
bg, en: qref |
|
|
end; |
|
|
procedure Init(var q: queue); |
{ Почати роботу } |
|
function Empty (q: queue): boolean; |
{ Чи порожня черга?} |
|
procedure Add (var q: queue; n: integer); |
{ Додати елемент до кінця черги } |
procedure Take (var q: queue; var n: integer);{Взяти елемент з початку черги} implementation
.................
procedure Add (var q: queue; n: integer); var p: qref;
begin
new(p); p^.d := n; p^.next := nil; if q.bg=nil then q.bg := p
36
else q.en^.next:=p; q.en := p
end;
procedure Take (var q: queue; var n: integer); var p: qref;
begin
if q.bg = nil then begin
writeln(' Take: Черга порожня'); halt end;
p:=q.bg; n:=q.bg^.d; q.bg:=q.bg^.next;
if q.bg=nil then q.en:=nil; dispose(p)
end;
end.
Підпрограми Init та Empty є дуже простими та майже не відрізняються від аналогічних підпрограм для стеку. Більшу увагу необхідно приділити додаванню та взяттю елемента черги.
Змінення черги під час дії „додати елемент” (процедура Add) зображені на рис. 3.10. На відміну від стеків, для черг треба окремо розглядати випадок додавання елемента до непорожньої черги (рис. 3.10 а-в) та порожньої черги (рис. 3.10 г-е). Після команди new(p) виділяється пам’ять під новий елемент черги (рис. 3.10 а, г). Ланцюг p^.d:=n; p^.next:=nil забезпечує запис числа n у поле даних та значення nil у поле вказівника на наступний елемент черги (рис.
3.10б, д). Якщо черга була порожньою (q.bg=nil), то доданий елемент буде також першим елементом черги (q.bg:=p), інакше треба пов’язати останній елемент, на який вказує q.en, з новим елементом черги (q.en^.next:=p). Після чого, треба змінити вказівник на кінець черги (q.en:=p). Останні дії зображено на рис. 3.10 в), е).
Змінення черги під час дії „взяти елемент” (процедура Take) зображені на рис. 3.11. Якщо черга порожня, то виводиться повідомлення про помилку, а програма аварійно завершує роботу. Якщо ж черга не порожня, то треба окремо розглядати два випадки: черга складається більше ніж з одного елемента (рис.
3.11а-в), та черга складається з одного елемента (рис. 3.11 г-е). Після ланцюга p:=q.bg; n:=q.bg^.d (рис. 3.11 а, г) треба запам’ятати вказівник на перший елемент черги та зчитати дані з цього елемента. Присвоєння q.bg:=q.bg^.next у першому випадку переміщує вказівник q.bg на другий елемент черги (рис. 3.11 б), а у другому – присвоює йому значення nil (рис. 3.11 д). Якщо тепер q.bg=nil, тобто черга складалась з 1 елемента, треба q.en:=nil, бо черга стає порожньою. Нарешті, в обох випадках потрібно звільнити пам’ять, яку займав перший елемент черги (dispose(p)). Останні дії зображено на рис. 3.11 в), е).
37

Рисунок 3.10 - Додавання елемента до кінця черги
Як приклад роботи з чергою можна навести задачу присвоєння черзі q1 значення черги q2, використовуючи тільки дії над чергами. Виконати це присвоєння просто для записів (q1:=q2) некоректно, оскільки подальші зміни кожної з черг q1 або q2 будуть впливати на іншу. Тому треба скопіювати всі елементи q2, створивши нову чергу q1. Відповідна програма QueueTst наведена нижче. У процедурі SetQueue змінні q1 та q2 описано як параметри-змінні тому, що запис q2 змінюється у процедурі.
38

Рисунок 3.11 - Взяття елемента з початку черги
Справа в тому, що склад дій над чергою не дозволяє скопіювати чергу, не знищивши її. Для відновлення черги q2 використовується допоміжна черга q3. Після відновлення вказівники на початок та кінець черги у q2 будуть вказувати можливо на іншу область пам’яті, ніж перед викликом процедури. Але сама черга буде складатись з тих самих елементів у тій самій послідовності, що і до виклику. Процедура ShowQueue показує чергу, знищуючи її. Ця процедура демонструє, що присвоєнння правильно копіює елементи черги q2 у q1, не знищуючи чергу q2.
Program QueueTst; uses IntQueue; {q1:=q2}
procedure SetQueue(var q1, q2: queue); var q3: queue;
n: integer; begin
Init(q3);
while not Empty(q2) do begin Take(q2,n);
39
Add(q1,n); Add(q3,n); end;
while not Empty(q3) do begin Take(q3,n);
Add(q2,n);
end;
end;
{Показати чергу}
procedure ShowQueue(var q: queue); var n: integer;
begin
while not Empty(q) do begin Take(q,n);
write(n,' ') end; writeln
end;
var q1,q2: queue; n: integer; i,m: word;
begin
Init(q2);
writeln('Кількість елементів черги'); readln(m);
for i:=1 to m do begin write('Елемент ',i,' = '); readln(n);
Add(q2,n)
end;
SetQueue(q1,q2); write('q2 = '); ShowQueue(q2); write('q1 = '); ShowQueue(q1);
end.
3.4. Дек
Дек або двостороння черга – це лінійний список, в якому всі операції вставки та видалення (і, як правило, операції доступу до даних) виконуються на обох кінцях списку.
Дек є найбільш загальним варіантом стеку або черги. Крім того, слід розрізняти деки з обмеженим виведенням та з обмеженим введенням, в яких
40

операції видалення та вставки елементів відповідно виконуються тільки на одному з кінців.
Дек визначається як:
1Порожній дек.
2Перший елемент; дек.
3Дек; останній елемент.
Дек можна представити, як сукупність однотипних елементів, в якій є доступ до початку або кінця деку для додавання або взяття елементів (рис. 3.12).
Рисунок 3.12 - Дек
Операції, відношення та інструкції для деків:
1Почати роботу.
2Чи порожній дек?
3Додати елемент до початку деку.
4Взяти елемент з початку деку.
5Додати елемент до кінця деку.
6Взяти елемент з кінця деку.
Дії 1, 3, 4, 5, 6 – інструкції; 2 – відношення. “Почати роботу” означає створити порожній дек.
“Додати елемент до початку деку” – додати до деку один елемент, який стає першим у деку.
“Взяти елемент з початку деку” – взяти та повернути значення першого елемента. Першим стає наступний елемент деку або дек стає порожнім. Для порожнього деку ця інструкція повинна давати відмову.
“Додати елемент до кінця деку” – додати до деку один елемент, який стає останнім у деку.
“Взяти елемент з кінця деку” – взяти та повернути значення останнього елемента. Першим стає попередній елемент деку або дек стає порожнім. Для порожнього деку ця інструкція повинна давати відмову.
Можлива реалізація деку з використанням вказівників зображена на рис. 3.13. Елементи деку – це записи з трьох полів: поле даних та вказівники на наступний та попередній елементи. За допомогою вказівників елементи деку з’єднані у ланцюг. У останньому елементі вказівник на наступний елемент дорівнює nil, а у першому елементі вказівник на попередній елемент дорівнює nil. Сам дек реалізовано, як запис з двох вказівників: на початок деку та на кінець деку. Якщо дек порожній, то елементів немає, а обидва вказівники дорівнюють nil.
41

Рисунок 3.13 - Реалізація деку
Для деків реалізація їх інтерфейсної частини мовою Паскаль буде мати такий вигляд.
unit IntDeque; |
|
|
interface |
|
|
type dref = ^delem; |
{Вказівник на елемент деку} |
|
delem = record |
{Елемент деку} |
|
d: integer; |
|
|
next, prev: dref |
|
|
end; |
|
|
deque = record |
{Дек} |
|
bg, en: dref |
|
|
end; |
|
|
procedure Init(var d: deque); |
{Почати роботу} |
|
function Empty (d: deque): boolean; |
{Чи порожній дек?} |
|
procedure PutBg (var d: deque; n: integer); |
{Додати елемент до початку деку} |
procedure GetBg (var d: deque; var n: integer); {Взяти елемент з початку деку} procedure PutEn (var d: deque; n: integer); {Додати елемент до кінця деку} procedure GetEn (var d: deque; var n: integer); {Взяти елемент з кінця деку} implementation
.................
end.
3.5 Різновиди списків
Стеки, черги та деки призначені для тимчасового збереження та почергової обробки даних. Не можна отримати другий елемент черги, не змінивши її (для цього потрібно спочатку взяти перший елемент). В той же час, у багатьох задачах дані, що мають динамічний розмір, повинні багаторазово переглядатись та певним чином оброблятись. Для таких цілей використовують списки. Існує декілька видів списків, які мають спільні властивості, але відрізняються способами реалізації та складом допустимих дій.
42