- •Лінійні списки.
- •Стеки і черги
- •Стисле та індексне зберігання лінійних списків.
- •Індивідуальні завдання.
- •Лабораторна робота №11. Методи розробки алгоритмів.
- •Рекомендована література:
- •Теоретичні відомості.
- •Індивідуальні завдання.
- •Лабораторна робота №12. Застосування рекурсії при побудові алгоритмів.
- •Рекомендована література.
- •Індивідуальні завдання.
Стеки і черги
Стек - це лінійний список, де операції додавання і вилучення елемента та доступу до елемента виконуються тільки в його кінці.
Черга -це лінійний список, в якому елементи вилучаються з початку списку, а додаються в його кінці.
Двостороння черга - це лінійний список, в якому операції можна виконувати і на початку, і в кінці.
У роботі частіше зустрічаються стеки. Наведемо приклад застосування стеків при обчисленні виразів. Однією з форм подавання виразів є бездужковий польський інверсний запис, в якому порядок виконання операції визначається її контекстом та позицією у виразі. Вираз задається так, що операції в ньому записуються в порядку їх виконання, а операнди містяться безпосередньо перед операцією; елементи запису розділяються пропуском чи комою. Наприклад, вираз (5+7)*3-4*2 в польському інверсному записі маї вигляд 5, 7, +, 3, *, 4, 2, *, -. Особливість польського інверсного запису полягає в тому, що значення виразу можна обчислити за один його перегляд зліва направо, використовуючи стек (стек спочатку повинен бути порожнім). Якщо при перегляді виразу з'являються дані, то вони заносяться в стек, а якщо з'явиться операція, то вона виконується над верхніми елементами стека з заміною їх результатом обчислення. Ілюстрація динаміки зміни стека для виразу 5, 7, +, 3, *, 4, 2, *, - маї вигляд: st=<>--<5>--<5, 7>--<12>--<12, 3>--<36>--<36, 4>--<36, 4 ,2>--<36, 8>--<28>.
Функція seval () обчислює значення виразу, заданого в масиві a[n] у польському інверсному записі, причому a[i]>=0 означаї невід'їмне ціле число, a[i]<0-операцію. Для кодування операцій додавання, віднімання, множення і ділення вибрані відповідно від'їмні числа -1, -2, -3, -4. Для організації послідовного зберегання стека використовується масив цілих st[100] і ціла змінна t, що фіксує останній елемент стека У функції seval не реалізована перевірка стека на порожність, переповнення (t=-1, t=100) та на правильність вхідної інформації. Текст цієї функції є таким:
/*seval-обчислення виразу в польському інверсному записі*/
seval (a,n)
int a[], n;
{ int st [100], p, t, c;
t=-1;
for (p=0; p<n; p++)
if (a[p]<0)
{ c=st[t- -];
swich (a[p])
{
case -1: st[t]+ =c; break;
case -2: st[t]- =c; break;
case -3: st[t]* =c; break;
case -4: st[t]/ =c; break; }
}
else st[++t]=a[p];
return st[t];
}
Послідовне зберігання стеків і черг. Припустемо, що для розміщення потрібних в задачі стеків та черг цілих чисел є послідовна ділянка пам'яті - масив d з M елементами, задана описом:
# define M 100
int d[M];
Реалізація одного стека здіснюється так: перший елемент стека розміщюється на початку області d, а індекс його останнього елемента відмічається значенням змінної a, яке спочатку дорівнюї -1, що означає порожнўй стек, тобто має бути опис int a=-1;. Додавання елемента в стек виконується оператором d[++a]=v; вилучення зі стека - оператором v=d[a--]. Для реалізації двох стеків можна поділити область d на дві ділянки розмірів M1 та M2 (M1+M2=M) і в кожній з них розміщати окремий стек. Але це недоцільно, оскільки в одному зі стеків може настати переповнення, а в другому - пам'ять буде вільна. Тому доцільніше перший стек розміщувати з початку області d, фіксуючи його вершину значенням змінної a, а другий стек-з кінця області d, фіксуючи його вершину в змінній b, тобто мати оголошення int a=-1, b=M; і стеки рухати назустріч один одному. Операції вилучення та додавання елементів в стеки тепер здіснюються так: вилучення елемента з першого стека - v=d[a--]; з другого стека - v=d[b++]; додавання елемента в перший стек - d[++a]=new; в другий - d[--b]=new. Переповнення області d настає за виконанням умови a=b-1.
Якщо в області d повинно бути більше двох стеків, то організація їх керування неможлива без перерозподілу пам'яті, тобто не уникнути ситуації, коли в одному зі стеків настає переповнення за наявності в d вільних ділянок. При перерозподілі пам'яті між стеками доводиться більшість стеків переміщати (як правило, до 10% залишиної вільної пам'яті ділити рівномірно між всіма стеками, решту - пропорційно збільшенню стека за час від останнього перерозподілу).
При реалізації черги в масиві d її перший елемент розміщується на початку d. Для фіксації значень індексів початку та кінця черги використовуються цілі змінні a і b з початковими значеннями a=0; b=-1, тобто потрібний опис int a=0, b=-1;. Вилучення елемента з непустої черги здійснюється оператором v=d[a++]; додавання елемента в чергу -оператором d[++b]=v; черга стає порожньою, коли a>b, тобто a=b+1. Досягнення кінця області d, коли b=M-1, вимагає перерозподілу пам'яті, тобто зміщення черги до лівогокраю області d. Якщо черга стає порожньою, то для зменшення таких перерозподілів часто беруть a=0; b=1. Можна уникнути перерозподілів, організуючи в області d чергу як циклічну структуру, в якій слідом за останнім елементом області йде її перший елемент (...d[M-1], D[0], D[1], ...). За використання циклічної структури рівність a=b+1 буде вірна для порожньої черги або для заповненої. Щоб розрізняти ці черги, необхідно додатково мати інформацію про кількість елементів у них.
Організація в d двох або більше черг неможлива без переміщень і перерозподілів пам'яті. Реалізація двосторонніх черг з послідовним зберіганням аналогічна реалізації звичайних черг.
Зв'язане зберігання стеків та черг. Проблеми переміщення і перерозподілу пам'яті, коли стеків n>=3 або черг n>=2, зникають при зв'язаному зберіганні. Стеки реалізуються аналогічно лінійним спискам з покажчиком на кінець списку і зв'язком від наступного елемента до попереднього. Описом стека та його ініціацією є: LISTP dl=NULL;.
Функцўя pop вилучення елемента зі стека і функція add додавання нового елемента в стек з формальним параметром типу LISTP* змінюють значення покажчика на останній елемент стека. Крім того, функція pop повертає значення вилученого елемента стека. Текст цих функцій записується у вигляді:
/*pop-вилучення елемента зі стека *dl*/
pop (dl)
LISTP *dl;
{
int v; LISTP q;
if (q=*dl)
{ v=q->val; *dl=q->next; free (q);
return v;
}
else error ("немаї елемента");
}
/*add-додаванняв стек *dl елемента v*/
add (dl,v)
LISTP *dl; int v;
{ LISTP new ();
*dl=new (v,dl);
}
Черги релізуються аналогічно лінійним спискам, але з двома покажчиками LISTP dl, e; на перший та останній елементи. Поро жня черга реалўзуїться оператором dl=e=NULL. Функцўя addq додавання нового елемента в чергу і функція popq вилучення елемента з черги з двома формальними параметрами типу LISTP* мають вигляд:
/*addq-додавання в чергу елемента v*/
addq (dl, e, v)
LISTP *dl, *e; int v;
{
LISTP q, new ();
q=new (v, NULL);
if (*dl) (*e)->next=q;
else (*dl)=q;
*e=q;
}
/*popq-вилучення елементўв з черги */
popq (dl, e)
LISTP *dl, *e;
{
int v; LISTP q;
if (!(q=*dl)) error ("немаї елемента");
if (q==*e)
*dl=*e=NULL;
else (*dl)=q->next;
v=q->val; free (q);
return v;
}
Операції з чергою можна спростити, якщо додати до не “голову”-вузол з інформацією, що не використовується Тоді порожня черга формується оператором dl=e=new (0, NULL), а функції addqh і popqh реалізують операції додавання елемента в чергу та вилучення елемента з неї:
/*addqh - додавання елемента в чергу з “головою” */
addqh (dl, e, v);
LISTP *dl, *e;
int v;
{
LISTP new (), q;
q= new (v, NULL);
(*e)-> next;
}
/* popqh - вилучення елемента з черги з “головою” */
popqh (dl, e, v);
LISTP *dl, *e;
{
LISTP q;
if (*dl == *e) error (“немає елемента”);
q = *dl;
*dl = q->next;
free (q);
return (*dl)->val;
}
Чергу можна утворити, використовуючи циклічний список; тоді досить одного покажчика dl.
Двостороння черга реалізується аналогічно звичайній. У ній легко додавати елементи з обох кінців та вилучати елементи з початку, але важко вилучати елемента з кінця.