Динамічні структури даних: списки h2>
Вступ h2>
В
мовах програмування (Pascal, C, ін) існує й інший спосіб виділення
пам'яті під дані, який називається динамічним. У цьому випадку пам'ять під
величини відводиться під час виконання програми. Такі величини будемо називати
динамічними. Розділ оперативної пам'яті, розподіляється статичний, називається
статичної пам'яттю; динамічно розподілена розділ пам'яті називається
динамічною пам'яттю (динамічно розподіленою пам'яттю). p>
Використання
динамічних величин надає програмісту ряд додаткових
можливостей. По-перше, підключення динамічної пам'яті дозволяє збільшити
обсяг оброблюваних даних. По-друге, якщо потреба в якихось даних
відпала до закінчення програми, то зайняту ними пам'ять можна звільнити для
іншої інформації. По-третє, використання динамічної пам'яті дозволяє
створювати структури даних змінного розміру. p>
Робота
з динамічними величинами пов'язана з використанням ще одного типу даних --
посилального типу. Величини, що мають контрольний тип, називають покажчиками. P>
Покажчик
містить адресу поля в динамічній пам'яті, що зберігає величину певного
типу. Сам покажчик розташовується в статичної пам'яті. P>
Адреса
величини - це номер першого байта поля пам'яті, в якому розташовується
величина. Розмір поля однозначно визначається типом. P>
Далі
будемо більш докладно обговорювати покажчики і дії з ними в мові Pascal,
приклади будемо приводити на Pascal та C. p>
Величина
посилального типу (покажчик) описується в розділі опису змінних наступним
так: p>
Var <ідентифікатор>: ^ <ім'я типу>; p>
Ось
приклади опису покажчиків: p>
Type Mas1 = Array [1 .. 100] Of
Integer; p>
Var
P1: ^ Integer; p>
P2: ^ String; p>
Pm: ^ Mas1; p>
Тут
P1 - покажчик на динамічну величину цілого типу; P2 - покажчик на
динамічну величину строкового типу; Pm - покажчик на динамічний масив,
тип якого задано у роздiлi Type. p>
Самі
динамічні величини не вимагають опису в програмі, оскільки під час
компіляції пам'ять під них не виділяється. Під час компіляції пам'ять виділяється
тільки під статичні величини. Покажчики - це статичні величини, тому
вони вимагають опису. p>
Яким
же чином відбувається виділення пам'яті під динамічну величину? Пам'ять під
динамічну величину, пов'язану з покажчиком, виділяється в результаті
виконання стандартної процедури NEW. Формат звернення до цієї процедури: p>
NEW (<покажчик >); p>
Вважається,
що після виконання цього оператора створена динамічна величина, ім'я якої
має такий вигляд: p>
<ім'я динамічної величини>: =
<покажчик> ^ p>
Нехай
в програмі, в якій є наведене вище, присутні
наступні оператори: p>
NEW (P1); NEW (P2); NEW (Pm); p>
Після
їх виконання у динамічній пам'яті виявляється виділеним місце під три
величини (два скалярні і один масив), які мають ідентифікатори: p>
P1 ^,
P2 ^, Pm ^ p>
Наприклад,
позначення P1 ^ можна розшифрувати так: Динамічна змінна, на яку
посилається вказівник P1. p>
Подальша
робота з динамічними змінними відбувається точно так само, як зі статичними
змінними відповідних типів. Їм можна присвоювати значення, їх можна
використовувати в якості операндів у висловах, параметрів підпрограм і пр.
Наприклад, якщо змінної P1 ^ потрібно присвоїти число 25, змінної P2 ^
присвоїти значення символу "Write", а масив Pm ^ заповнити по порядку
цілими числами від 1 до 100, то це робиться так: p>
P1 ^: = 25; p>
P2 ^: = 'Write'; p>
For I: = 1 To 100 Do Pm ^ [I]: = I; p>
Крім
процедури NEW значення покажчика може визначатися оператором присвоювання: p>
<покажчик>: = <Посилальне
вираз>; p>
В
як посилального виразу можна використовувати p>
покажчик;
p>
посилальну
функцію (тобто функцію, значенням якої є покажчик); p>
константу
Nil. p>
Nil
- Це зарезервований константа, що позначає порожню посилання, тобто посилання,
яка ні на що не вказує. При присвоєнні базові типи покажчика та
посилального вирази повинні бути однакові. Константу Nil можна привласнювати
вказівником з будь-яким базовим типом. p>
До
присвоювання значення посилальної змінної (за допомогою оператора присвоєння
або процедури NEW) вона є невизначеною. p>
Введення
і висновок покажчиків не допускається. p>
Розглянемо
приклад. Нехай у програмі описані наступні покажчики: p>
Var D, P: ^ Integer; p>
K: ^ Boolean; p>
Тоді
допустимими є оператори присвоювання p>
D: = P; K: = Nil; p>
оскільки
дотримується принцип відповідності типів. Оператор K: = D помилковий, тому що базові
типи у правій та лівій частині різні. p>
Якщо
динамічна величина втрачає свій покажчик, то вона стає
"сміттям". У програмуванні під цим словом розуміють інформацію,
яка займає пам'ять, але вже не потрібна. p>
Уявіть
собі, що в програмі, в якій присутні описані вище покажчики, в
розділі операторів записано наступне: p>
NEW (D);
NEW (P); p>
(Виділено
місце в динамічній пам'яті під дві цілі змінні. Покажчики отримали
відповідні значення) p>
D ^
: = 3; P ^: = 5; p>
(Динамічним
змінним присвоєно значення) p>
P
: = D; p>
(Покажчики
P і D стали посилатися на одну й ту ж величину, що дорівнює 3) p>
WriteLn (P ^,
D ^); (Двічі друкується число 3) p>
Таким
чином, динамічна величина, що дорівнює 5, втратила свій покажчик і стала
недоступною. Однак місце в пам'яті вона займає. Це і є приклад
виникнення "сміття". На схемі показано, що відбулося в результаті
виконання оператора P: = D. p>
В
Паскалі є стандартна процедура, що дозволяє звільняти пам'ять від
даних, потреба в яких відпала. Її формат: p>
DISPOSE (<покажчик >); p>
Наприклад,
якщо динамічна змінна P ^ більше не потрібна, то оператор p>
DISPOSE (P) p>
видалить
її з пам'яті. Після цього значення покажчика P стає невизначеним.
Особливо істотним стає ефект економії пам'яті при видаленні великих
масивів. p>
В
версіях Турбо-Паскаля, що працюють під операційною системою MS DOS, під дані
однієї програми виділяється 64 кілобайт пам'яті (або, якщо точніше, 65520
байт). Це і є статична область пам'яті. При необхідності працювати з
великими масивами інформації цього може виявитися мало. Розмір динамічної
пам'яті - набагато більше (сотні кілобайт). Тому використання динамічної
пам'яті дозволяє істотно збільшити обсяг оброблюваної інформації. p>
Слід
чітко розуміти, що робота з динамічними даними уповільнює виконання
програми, оскільки доступ до величини відбувається у два кроки: спочатку шукається
покажчик, потім по ньому - величина. Як це часто буває, діє
"закон збереження неприємностей": виграш в пам'яті компенсується
програшем в часі. p>
Приклад.
Дан текстовий файл розміром не більше 64 Кб, що містить дійсні числа, за
одному в кожному рядку. Переписати вміст файлу в масив, розмістивши його в
динамічно розподіляє пам'яті. Обчислити середнє значення елементів
масиву. Очистити динамічну пам'ять. Створити цілий масив розміром 10000,
заповнити його випадковими цілими числами в діапазоні від -100 до 100 і обчислити
його середнє значення. p>
(Мова Turbo Pascal) p>
Program Srednee; p>
Const NMax = 10000; p>
Type Diapazon = 1 .. NMax; p>
MasInt = Array [Diapazon] Of Integer; p>
MasReal = Array [Diapazon] Of Real; p>
Var PIint: ^ MasInt; PReal:
^ MasReal; p>
I, Midint: longInt; MidReal: Real;
T: Text; S: string; p>
Begin p>
Write ( 'Введіть ім'я
файлу: '); ReadLn (S); p>
Assign (T, S); Reset (T); MidReal: =
0; MidInt: = 0; p>
Randomize;
p>
NEW (PReal);
(Виділення пам'яті під речовинний масив) p>
(Введення
і підсумовування речового масиву) p>
While Not Eof (T) Do p>
Begin ReadLn (T, PReal ^ [I]); MidReal
: = MidReal + PReal ^ [I] End; p>
DISPOSE (PReal);
(Видалення речового масиву) p>
NEW (PInt);
(Виділення пам'яті під цілий масив) p>
(Обчислення
і підсумовування цілого масиву) p>
For I: = 1 To NMax Do p>
Begin PInt ^ [I]: = -100 +
Random (201); MidInt: = MidInt + PInt ^ [I] End; p>
(Висновок
середніх значень) p>
WriteLn ( 'середнє
ціле одно: ', MidInt Div NMax); p>
WriteLn ( 'середнє
речовий одно: ', (MidReal/NMax): 10: 6) p>
End. p>
// Мова C + + p>
# include p>
# include p>
# include p>
# include p>
# define NMax 10000 p>
typedef int MasInt; p>
typedef float MasReal; p>
MasInt * PInt; MasReal * PReal; p>
int I, n, MidInt; float MidReal;
char S [255]; p>
FILE * t; char * endptr; p>
void main () p>
(
cout << "Введіть
назва файлу: "; cin>> S; p>
t = fopen (S, "r"); p>
MidReal = 0; MidInt = 0; p>
randomize (); I = 0; p>
/* Виділення пам'яті під речовинний
масив */ p>
PReal = (MasReal *) malloc (sizeof (MasReal)); p>
/ * Введення і підсумовування речового масиву */ p>
while (! feof (t)) p>
(fgets (S, 255, t);// вводимо з файлу
рядок p>
PReal [I] = strtod (S, & endptr);//
перетворимо введену рядок у дійсне число p>
MidReal + = PReal [I]; I + +;) p>
n = I +1; p>
free (PReal);/* Видалення речового масиву */ p>
PInt = (MasInt *) malloc (sizeof (MasInt));
/ * Виділення пам'яті під цілий масив */ p>
/* Обчислення і підсумовування цілого
масиву */ p>
for (I = 0; I
(PInt [I] = -100 + random (201); p>
MidInt + = PInt [I];) p>
/ * Висновок середніх значень */ p>
cout << "nсреднее ціле одно
"<
cout << "середнє речовий
одно: "<
fclose (t); p>
) p>
Списки
p>
Обговоримо
питання про те, як у динамічній пам'яті можна створити структуру даних
змінного розміру. p>
Розберемо
Наведемо приклад. У процесі фізичного експерименту часто знімаються
показання приладу (припустимо, термометра) і записуються в комп'ютерну пам'ять
для подальшої обробки. Наперед невідомо, скільки буде вироблено
вимірювань. p>
Якщо
для обробки таких даних не використовувати зовнішню пам'ять (файли), то розумно
розташувати їх у динамічній пам'яті. По-перше, динамічна пам'ять дозволяє
зберігати більший обсяг інформації, ніж статична. А по-друге, у динамічній
пам'яті ці числа можна організувати в зв'язаний список, який не вимагає
попереднього зазначення кількості чисел, подібно до масиву. Що ж таке
"зв'язаний список"? Схематично він виглядає так: p>
p>
Тут
Inf - інформаційна частина ланки списку (величина будь-якого простого або
структурованого типу, крім файлового), Next - покажчик на наступну ланку
списку; First - покажчик на головній ланка списку. p>
Згідно
визначенням, список розташовується в динамічно пам'яті, в
статичної пам'яті зберігається лише вказівник на головній ланка. Структура, в
відміну від масиву, є дійсно динамічної: ланки створюються і
видаляються в міру необхідності, в процесі виконання програми. p>
Для
оголошення списку зроблено виняток: вказівник на ланка списку оголошується
раніше, ніж саме ланка. У загальному вигляді оголошення виглядає так. p>
Type U
= ^ Zveno; p>
Zveno = Record Inf: BT; Next: U End; p>
Тут
BT - деякий базовий тип елементів списку. p>
Якщо
покажчик посилається лише на наступну ланку списку (як показано на малюнку і
в оголошеній вище структурі), то такий список називають односпрямованим, якщо
на наступне та попереднє ланки - двонаправленим списком. Якщо покажчик у
останньому ланці встановлений не в Nil, а посилається на головній ланка списку, то
такий список називається кільцевих. Кільцевими можуть бути і односпрямовані, і
двонаправлені списки. p>
Більше
докладно розглянемо роботу зі зв'язаними списками на прикладі однонаправленої
некольцевого списку. p>
Виділимо
типові операції над списками: p>
додавання
ланки в початок списку; p>
видалення
ланки з початку списку; p>
додавання
ланки в довільне місце списку, відмінне від початку (наприклад, після ланки,
вказівник на яке задано); p>
видалення
ланки з довільного місця списку, відмінного від початку (наприклад, після
ланки, покажчик на який заданий); p>
перевірка,
порожній чи список; p>
очищення
списку; p>
друк
списку. p>
Реалізуємо
виділений набір операцій у вигляді модуля. Підключивши цей модуль, можна вирішити
більшість типових завдань на обробку списку. Нехай список оголошений так, як
було описано вище. Перші чотири дії спочатку реалізуємо окремо, забезпечивши
їх ілюстраціями. p>
1.
Додавання ланки в початок списку p>
p>
(Процедура долучення
ланки в початок списку; в x міститься додається інформація) p>
Procedure V_Nachalo (Var First: U; X: BT); p>
Var Vsp: U; p>
Begin p>
New (Vsp); p>
Vsp ^. Inf: = X; p>
Vsp ^. Next: =
First; (Те ланка, що було заголовним, стає другим за рахунком) p>
First: = Vsp;
(Нове ланка стає заголовним) p>
End; p>
2.
Видалення ланки з початку списку p>
p>
(Процедура видалення ланки
з початку списку; p>
в x міститься інформація
з віддаленого ланки) p>
Procedure Iz_Nachala (Var First: U; Var X:
BT); p>
Var Vsp: U; p>
Begin p>
Vsp: = First; (Забираємо посилання на поточне заголовної ланка) p>
First: = First ^. Next;
(Те ланка, що було другим за рахунком, стає заголовним) p>
X: = Vsp ^. Inf;
(Забираємо інформацію з видаляємого ланки) p>
Dispose (Vsp);
(Знищуємо ланка) p>
End; p>
3.
Додавання ланки в довільне місце списку, відмінне від початку (після ланки,
вказівник на яке задано) p>
p>
(Процедура долучення
ланки в список після ланки, p>
на яке посилається
покажчик Pred; p>
в x міститься інформація
для додавання) p>
Procedure V_Spisok (Pred: U; X: BT); p>
Var Vsp: U; p>
Begin p>
New (Vsp); (Створюємо пусте ланка) p>
Vsp ^. Inf: = X; (заносимо
інформацію) p>
Vsp ^. Next: =
Pred ^. Next; (Тепер ця ланка посилається на те, p>
що було слідом за ланкою Pred) p>
Pred ^. Next: = Vsp;
(Тепер нова ланка встало слідом за ланкою Pred) p>
End; p>
4.
Видалення ланки з довільного місця списку, відмінного від початку (після ланки,
вказівник на яке задано) p>
p>
(Процедура видалення ланки
зі списку після ланки, p>
на яке посилається
покажчик Pred; p>
в x міститься інформація
з віддаленого ланки) p>
Procedure Iz_Spiska (Pred: U; Var X: BT); p>
Var Vsp: U; p>
Begin p>
Vsp: = Pred ^. Next; (Забираємо посилання на видалити ланка) p>
(Видаляємо ланка зі списку,
перенаправивши посилання на наступне p>
за ним ланка) p>
Pred ^. Next: = Pred ^. Next ^. Next; p>
X: = Vsp ^. Inf; (Забираємо
інформацію з видаляємого ланки) p>
Dispose (Vsp); (Знищуємо ланка) p>
End; p>
Наведемо
повний текст модуля. p>
(Мова Pascal) p>
Unit Spisok; p>
Interface p>
Type BT = LongInt; p>
U = ^ Zveno; p>
Zveno = Record Inf: BT; Next: U
End; p>
Procedure V_Nachalo (Var First: U; X:
BT); p>
Procedure Iz_Nachala (Var First: U; Var
X: BT); p>
Procedure V_Spisok (Pred: U; X: BT); p>
Procedure Iz_Spiska (Pred: U; Var X:
BT); p>
Procedure Ochistka (Var First: U); p>
Function Pust (First: U): Boolean; p>
Procedure Print (First: U); p>
Implementation p>
p>
Procedure V_Nachalo; p>
Var Vsp: U; p>
Begin p>
New (Vsp); p>
Vsp ^. Inf: = X; p>
Vsp ^. Next: = First; p>
First: = Vsp; p>
End; p>
p>
Procedure Iz_Nachala; p>
Var Vsp: U; p>
Begin p>
Vsp: = First; p>
First: = First ^. Next; p>
X: = Vsp ^. Inf; p>
Dispose (Vsp); p>
End; p>
p>
Procedure V_Spisok; p>
Var Vsp: U; p>
Begin p>
New (Vsp); p>
Vsp ^. Inf: = X; p>
Vsp ^. Next: = Pred ^. Next; p>
Pred ^. Next: = Vsp; p>
End; p>
p>
Procedure Iz_Spiska; p>
Var Vsp: U; p>
Begin p>
Vsp: = Pred ^. Next; p>
Pred ^. Next: = Pred ^. Next ^. Next; p>
X: = Vsp ^. Inf; p>
Dispose (Vsp); p>
End; p>
p>
Procedure Ochistka; p>
Var Vsp: BT; p>
Begin p>
While Not Pust (First) Do
Iz_Nachala (First, Vsp) p>
End; p>
p>
Function Pust; p>
Begin p>
Pust: = First = Nil p>
End; p>
p>
Procedure Print; p>
Var Vsp: U; p>
Begin p>
Vsp: = First; p>
While Vsp <> Nil Do p>
Begin p>
Write (Vsp ^. Inf: 6); p>
Vsp: = Vsp ^. Next p>
End; WriteLn p>
End; p>
Begin p>
End. p>
p>
// Мова С + + p>
# include <
iostream.h> p>
# include p>
# include <
stdlib.h> p>
# include p>
typedef long
BT; p>
struct Zveno ( p>
BT Inf; p>
Zveno * Next;); p>
Zveno
* V_Nachalo (Zveno * First, BT X) p>
(Zveno * Vsp; p>
Vsp = (Zveno *) malloc (sizeof (Zveno));
p>
Vsp-> Inf = X; Vsp-> Next = First;
First = Vsp; p>
return First; p>
) p>
Zveno
* Iz_Nachala (Zveno * First) p>
(Zveno * Vsp; p>
Vsp = First-> Next; p>
free (First); p>
return Vsp; p>
) p>
Zveno * V_Spisok (Zveno
* Pred, BT X) p>
(Zveno * Vsp; p>
Vsp = (Zveno *) malloc (sizeof (Zveno));
p>
Vsp-> Inf = X; p>
Vsp-> Next = Pred-> Next; p>
Pred-> Next = Vsp; p>
return Vsp; p>
) p>
BT Iz_Spiska (Zveno
* Pred) p>
(BT X; p>
Zveno * Vsp; p>
Vsp = Pred-> Next; p>
Pred-> Next = Pred-> Next-> Next; p>
X = Vsp-> Inf; p>
free (Vsp); p>
return X; p>
) p>
void Print (Zveno
* First) p>
(Zveno * Vsp; p>
Vsp = First; p>
while (Vsp) p>
(cout < Inf <<
''; Vsp = Vsp-> Next;) p>
cout <<
"n"; p>
) p>
int Pust (Zveno
* First) p>
( p>
return! First; p>
) p>
Zveno * Ochistka (Zveno
* First) p>
( p>
while (! Pust (First))
First = Iz_Nachala (First); p>
return First; p>
) p>
Приклад.
Скласти програму, яка на основі визначеного списку формує два інших,
розміщуючи в першому з них позитивні, а в другій - негативні елементи
вихідного списку. p>
При
реалізації алгоритму будемо використовувати підпрограми розробленого модуля. Це
істотно полегшує вирішення завдання. p>
(Програма
на Turbo Pascal) p>
Program
Ex_sp_1; p>
Uses Spisok; p>
Var S1, S2, S3, V1, V2, V3: U; A:
BT; I, N: Byte; p>
Begin p>
Randomize; p>
N: = 1 + Random (20); p>
S1: = Nil; A: = -100 + Random (201); p>
V_Nachalo (S1, A); V1: = S1; p>
For I: = 2 To N Do p>
Begin A: = -100 + Random (201); V_Spisok (V1, A); V1: = V1 ^. Next End; p>
WriteLn ( 'Простий список:'); Print (S1); p>
V1: = s1; S2: = Nil; S3: = Nil; p>
While V1 <> Nil Do p>
Begin p>
If V1 ^. Inf> 0 p>
Then If S2 = Nil p>
Then Begin V_Nachalo (S2, V1 ^. Inf);
V2: = S2 End p>
Else Begin V_Spisok (V2, V1 ^. Inf);
V2: = V2 ^. Next End; p>
If V1 ^. Inf <0 p>
Then If S3 = Nil p>
Then Begin V_Nachalo (s3, V1 ^. Inf);
V3: = S3 End p>
Else Begin V_Spisok (V3, V1 ^. Inf);
V3: = V3 ^. Next End; p>
V1: = V1 ^. Next p>
End; p>
WriteLn ( 'результуючий список з
позитивних елементів: '); Print (S2); p>
WriteLn ( 'результуючий список з
негативних елементів: '); Print (S3); p>
Ochistka (S1); Ochistka (S2); Ochistka (S3); p>
End. p>
// Програма на C + + p>
# include "SPIS.CPP" p>
void main () p>
(Zveno * S1, * S2, * S3, * V1, * V2, * V3;
p>
BT a; int i, n; p>
clrscr (); p>
randomize ();
p>
S1 = NULL;
p>
//
створюємо перший елемент p>
a =- 100 + random (201); p>
S1 = V_Nachalo (S1, a); p>
n = 1 + random (20);
p>
//
формуємо список довільної довжини і виводимо на друк p>
V1 = S1; p>
for (i = 2; i <= n; i + +) p>
( p>
a =- 100 + random (201); p>
V1 = V_Spisok (V1, a); p>
) p>
Print (S1); p>
V1 = S1; S2 = NULL; S3 = NULL; p>
while (V1) p>
(if (V1-> Inf> 0) p>
if (! S2) p>
(S2 = V_Nachalo (S2, V1-> Inf); V2 = S2;) p>
else (V_Spisok (V2, V1-> Inf); V2 = V2-> Next;); p>
if (V1-> Inf <0) p>
if (! S3) p>
(S3 = V_Nachalo (S3, V1-> Inf); V3 = S3;) p>
else (V_Spisok (V3, V1-> Inf); V3 = V3-> Next;); p>
V1 = V1-> Next;) p>
cout << "результуючий список з
позитивних елементів: n "; p>
Print (S2); p>
cout << "результуючий список з
негативних елементів: n "; p>
Print (S3); p>
S1 = Ochistka (S1); S2 = Ochistka (S2);
S3 = Ochistka (S3); p>
)
p>
Список літератури h2>
Для
підготовки даної роботи були використані матеріали з сайту http://comp-science.narod.ru
p>