Posibnik_Pascal
.pdfПитання для самоконтролю
1.Сформулюйте постановку задачі пошуку.
2.При вирішенні яких задач використовуються алгоритми пошуку?
3.У чому суть алгоритму послідовного пошуку?
4.Яка різниця в алгоритмах пошуку заданого елемента і пошуку мінімального елемента?
5.Яка ефективність алгоритму послідовного пошуку? У яких випадках вона досягається?
6.У чому суть алгоритму бінарного пошуку? Чому він так називається?
7.Який метод побудови алгоритмів використаний при створенні алгоритму бінарного пошуку?
8.Яким алгоритмом пошуку краще користуватися в упорядкованому масиві?
9.Яка ефективність алгоритму бінарного пошуку? У яких випадках вона досягається?
91
Лабораторна робота № 16
Тема: Однозв’язні лінійні списки.
Студент повинен знати: поняття вказівника та динамічної змінної, їх оголошення та ініціалізацію, синтаксис оголошення типу елемента однозв’язного лінійного списку, створення та ініціалізацію однозв’язного лінійного списку, операції добавлення та вилучення елементів списку.
Теоретичні відомості
Динамічні змінні – це змінні, для яких оперативна пам’ять виділяється під час виконання програми, і які знищуються також під час виконання програми. Область пам’яті, у якій розташовуються ці змінні, називається динамічною або купою (від англ. heap – купа). Звичайні (статичні) змінні характеризуються тим, що їх значення зберігаються в ділянках оперативної пам’яті, які визначаються на етапі компіляції програми і не змінюються під час її виконання. Обсяг оперативної пам’яті, що необхідний для збереження значення динамічної змінної, компілятору невідомий. Тому доступ до значення такої змінної здійснюється за її
адресою.
У мові Паскаль можна оголошувати статичні змінні спеціального виду, які називаються вказівниками. Значеннями вказівників можуть бути лише адреси комірок пам’яті. Розрізняють типізовані та нетипізовані вказівники. Синтаксис їх оголошення такий:
var <ім’я вказівника> : ^<тип>; <ім’я вказівника> : pointer;
Оголошення типізованого вказівника містить його тип, перед яким ставиться спеціальний символ ^ , а тип нетипізованого – pointer. Наприклад,
var X:^integer; Y: pointer;
Змінна Х є вказівником на тип integer, і цей вказівник типізований. Під час компіляції для Х буде визначена ділянка оперативної пам’яті для збереження адреси, бо значенням Х може бути тільки адреса. Оскільки Х - вказівник на тип integer, значенням Х може бути лише адреса тієї ділянки пам’яті, у якій міститься ціле число. Аналогічно, для Y теж буде визначена ділянка оперативної пам’яті для збереження адреси. Але тип значення та розмір ділянки, яку може адресувати Y, невідомий. Нетипізовані вказівники використовуються зазвичай для адресування якоїсь ділянки пам’яті. Розглянемо фрагмент програми.
var |
A:integer; |
X:^integer; |
|
begin |
A:=2; X:=@A; |
end. |
|
Зв’язок між А та Х зображений на малюнку |
|||
|
Змінна Х |
Змінна А |
|
Адреса А
2
На малюнку зображені дві ділянки оперативної пам’яті – одна для змінної Х і друга для А. При виконанні присвоювання A:=2 у пам’ять для змінної А буде занесене число 2. А після виконання присвоювання X:=@A у пам’ять для змінної Х буде занесена адреса ділянки пам’яті, що була виділена під А. Стрілка на малюнку показує те, що значенням вказівника Х є адреса ділянки пам’яті змінної А. У цілому цей малюнок зображає призначення вказівників
– показувати на ділянки пам’яті, у яких можуть знаходитися якісь значення.
Над вказівниками допустимі три види операцій: присвоювання, порівняння, розіменування. Вказівнику можна присвоювати лише адресу: змінної, підпрограми, константу-адресу (шістнадцяткове число), значення іншого вказівника. Якщо вказівнику присвоюється адреса якоїсь змінної, то це записується за таким синтаксисом
<ім’я вказівника> := @<ім’я змінної>;
92
Вказівники можна порівнювати, використовуючи лише операції = (рівність) та <> (нерівність). Операція розіменування застосовується до вказівника тоді, коли через вказівник треба отримати значення змінної, на яку він показує. Ця операція позначається символом «^» і записується після імені вказівника. Наприклад, для вказівника Х зображеного на малюнку, розіменування має вигляд Х^ і Х^ дорівнює 2. Оскільки розіменування - це значення певного типу, то воно може зустрічатися у виразах або на місці виразів. Зокрема, присвоювання
<змінна> := <ім’я вказівника>^; означає, що якійсь змінній присвоюється значення тієї змінної, на яку показує вказівник. А присвоювання
<ім’я вказівника>^ := <вираз>; означає, що змінній, на яку показує вказівник, присвоюється значення виразу. Таким чином
через вказівники можна отримувати доступ до значень змінних і за необхідності змінювати ці значення. Вказівникам можна присвоювати також спеціальне значення Nil, що означає порожню адресу.
При роботі з динамічними змінними вказівники можна ототожнювати з цими змінними. Нехай у розділі var оголошено вказівник X:^integer. А у виконуваній частині програми нехай зустрічається оператор New(X). Після виконання цього оператора у динамічній пам’яті буде виділена ділянка пам’яті, куди можна буде заносити ціле число. Адреса цієї ділянки буде присвоєна вказівнику Х. Якщо далі у програмі зустрінеться присвоювання X^:=5, то у виділену ділянку пам’яті буде занесене число 5. Отже, Х – це вказівник, а X^ - ніби змінна. Зазвичай при роботі з динамічною пам’яттю ім’я вказівника ототожнюють з іменем динамічної змінної, хоч завжди треба розрізняти семантичний зміст позначень Х та X^.
Робота з динамічною пам’яттю у системі Turbo Pascal здійснюється через виклики пар стандартних процедур New і Dispose або GetMem і FreeMem або Mark і Release. Процедури New і GetMem виділяють пам’ять під динамічну змінну, а Dispose і FreeMem звільняють пам’ять від змінної.
Списком називають послідовність елементів одного типу. Прикладом списку є масив. Але розмір масиву визначається на етапі компіляції і залишається незмінним протягом усього часу виконання програми. Якщо у списку часто потрібно виконувати добавлення чи вилучення елементів, то доцільно реалізовувати список у вигляді динамічної структури даних. Динамічний список можна організувати, використовуючи можливості мови Паскаль для створення динамічних структур даних.
Зв’язний лінійний список – це сукупність однотипних елементів, які послідовно зв’язані між собою за допомогою вказівників. Кожен елемент списку, крім останнього, містить вказівник на наступний елемент. Доступ до першого елемента здійснюється за допомогою вказівника на нього, а доступ до кожного наступного елемента – з використанням вказівника, який зберігається у попередньому елементі. Перший елемент списку називається його вершиною або головою.
Над зв’язними лінійними списками виконують такі дії:
•добавлення нового елемента у початок списку;
•добавлення нового елемента в кінець списку;
•добавлення нового елемента між двома наявними елементами списку;
•вилучення елемента зі списку.
Зв’язні лінійні списки поділяються на такі різновиди:
•однозв’язний лінійний список;
•однозв’язний циклічний список;
•двозв’язний лінійний список;
•двозв’язний циклічний список;
•стек;
•черга.
93
Однозв’язний лінійний список – це список, у якому попередній елемент вказує на наступний (попередній елемент містить вказівник на наступний). На малюнку зображено такий список.
head |
|
an |
|
|
an-1 |
|
|
a2 |
|
|
a1 |
Nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
Вказівник head показує на вершину списку, а елементи списку добавлялись до вершини, тобто список створювався справа наліво. Елементи а1, а2, …, аn – це дані списку.
Однозв’язний циклічний список – це однозв’язний лінійний список, у якому останній елемент списку вказує на перший. Такий список зображений на наступному малюнку.
head |
|
an |
|
|
an-1 |
|
|
a2 |
|
|
a1 |
|
|
|
|
|
|
|
|
|
|
|
|
Програмування однозв’язних лінійних списків починається з опису типу елемента списку. Елемент списку має структуру запису, у якому повинні бути передбачені поля для даних і одне поле для вказівника. Синтаксис опису типу має вигляд
type
ptr = ^element; |
{тип вказівника на елемент списку} |
element = record |
{тип елемента списку} |
data : <тип даних> |
{поле (поля) для даних} |
next : ptr; |
{вказівник на наступний елемент} |
end; |
|
var head, current : ptr;
У розділі var оголошений вказівник head на вершину списку, який потрібний для створення та роботи зі списком, а також current – вказівник на поточний елемент. На початку створення списку вказівнику head присвоюють значення Nil. Потім виконують операцію добавлення елемента до вершини списку необхідну кількість разів (можна добавляти елемент також іншим способом).
Операція добавлення нового елемента до списку полягає у тому, що спочатку в динамічній пам’яті створюється новий елемент, а потім він під’єднується до списку. Розглянемо різні варіанти цієї операції. На наступному малюнку зображено створення нового елемента, що добавлятиметься до вершини списку.
data Х
head |
|
an |
|
|
a1 |
Nil |
|
|
|
|
|
|
|
Тепер цей елемент під’єднаний до списку.
data Х
head |
|
an |
|
|
a1 Nil |
|
|
|
|
|
|
Операцію добавлення елемента до вершини списку можна реалізувати завдяки наступній процедурі.
procedure AddElemV(var head : ptr; elem:<тип>); var X:ptr;
begin
New(X); |
{виділення динамічної пам’яті для нового елемента Х} |
X^.data := elem; |
|
X^.next := head; |
{вказівник next тепер показує на вершину списку} |
head := X; |
{переадресування вказівника head на елемент Х} |
end;
94
Зобразимо на малюнку створення нового елемента, що буде добавлятися у кінець списку.
|
|
|
|
|
current |
|
|
|
|
data |
Nil |
Х |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
head |
|
an |
|
|
|
|
a1 |
Nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Після добавлення елемента список матиме вигляд.
|
|
|
|
|
|
current |
|
|
data Nil |
Х |
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
head |
|
an |
|
|
|
|
a1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Відповідна процедура має вигляд, якщо вважати current вказівником на останній |
||||||||||||
елемент списку. |
|
|
|
|
|
|
|
|
|
|
||
procedure AddElemK(var current : ptr; elem:<тип>); |
|
|
|
|||||||||
var X:ptr; |
|
|
|
|
|
|
|
|
|
|
||
begin |
|
|
|
|
|
|
|
|
|
|
||
|
New(X); |
|
|
{виділення динамічної пам’яті для нового елемента Х} |
||||||||
|
X^.data := elem; |
|
|
|
|
|
|
|
|
|||
|
X^.next := Nil; |
{вказівник next порожній} |
|
|
|
|||||||
|
current^.next := X; |
{зв’язування елемента Х із попереднім елементом а1} |
||||||||||
|
current := X; |
{переадресування вказівника current на елемент Х} |
||||||||||
end;
Тепер зобразимо на малюнку створення нового елемента, що буде добавлятися між двома наявними елементами списку.
|
current |
data |
Х |
|
|
head |
an |
ai |
ai-1 |
a1 |
Nil |
Після добавлення елемента список матиме вигляд. |
|
|
|
||
|
current |
data |
Х |
|
|
head |
an |
ai |
ai-1 |
a1 |
Nil |
Відповідна процедура має вигляд, якщо вважати current вказівником на i-й елемент списку.
procedure AddElemS(var current : ptr; elem:<тип>); var X:ptr;
begin
New(X); |
{виділення динамічної пам’яті для нового елемента Х} |
|
X^.data := elem; |
|
|
X^.next := current^.next; |
{вказівник next показує на (i-1)-й елемент} |
|
current^.next := X; |
{зв’язування елемента Х з попереднім і-м елементом} |
|
current := X; |
|
{переадресування вказівника current на елемент Х} |
end;
Аналогічно тому, як існують різні варіанти добавлення елемента до списку, також можна розглядати різні варіанти вилучення елемента. Розглянемо тільки вилучення елемента
95
у середині списку. Внісши необхідні уточнення, можна отримати реалізацію вилучення із вершини чи кінця списку.
current X
ai+1 |
ai |
ai-1 |
На малюнку зображені три елементи списку – (і+1)-й, і-й, (і-1)-й. Припустимо, що ми визначили (і+1)-й елемент, і нехай його вказівник current. Треба вилучити і-й елемент. Для цього спочатку треба визначити вказівник Х на і-й елемент. Це робиться через присвоювання
X:=current^.next.
current X
ai+1 |
ai |
ai-1 |
Наступним кроком операції вилучення є зміна посилання (і+1)-го елемента так, як показано на малюнку. Це робиться через присвоювання current^.next:=current^.next^.next. Тепер можна вилучати і-й елемент. Отримаємо такий малюнок.
current
|
ai+1 |
|
|
|
ai-1 |
|
|
|
|
|
|
|
|
||
Операцію вилучення можна реалізувати наступною процедурою. |
|||||||
procedure DelElem(var current : ptr); |
|
|
|
|
|||
var X:ptr; |
|
|
|
|
|
|
|
begin |
|
|
|
|
|
|
|
X := current^.next; |
{вказівник Х показує на i-й елемент} |
||||||
current^.next := current^.next^.next; |
{(і+1)-й елемент показує на (i-1)-й} |
||||||
Dispose(X); |
{вилучення і-го елемента} |
||||||
end;
Цю процедуру можна модифікувати для вилучення елемента з вершини чи кінця списку. Для пошуку якогось елемента у списку чи дослідження всіх його елементів можна
скористатися наступним фрагментом програми.
current := head; |
{початкове значення поточного вказівника current } |
while current<>Nil |
do |
begin |
|
{дослідити елемент списку}
current:=current^.next; |
{перехід до наступного елемента списку} |
end; |
|
96
Приклад
Створити список цілих чисел. Вивести на екран елементи списку, які знаходяться на парних місцях.
program lin_sp; |
|
uses crt; |
|
type |
|
spisok=^elem; |
{тип вказівника на елемент списку} |
elem=record |
{тип елемента списку} |
data:integer; |
{поле для цілого числа} |
next:spisok; |
{поле вказівника на наступний елемент списку} |
end; |
|
var |
l:spisok; |
|
|
{вказівник на список} |
|
|
{----------------------- |
|
процедура добавлення елемента до кінця списку --------------------------- |
} |
|||
procedure AddElemK(var current : spisok; elem: integer); |
|
|||||
var |
X:spisok; |
|
|
|
|
|
begin |
|
|
|
|
|
|
|
New(X); |
|
{виділення динамічної пам’яті для нового елемента Х} |
|||
|
X^.data := elem; |
|
|
|
||
|
X^.next := Nil; |
{вказівник next порожній} |
|
|||
|
current^.next := X; {зв’язування елемента Х із останнім елементом} |
|
||||
end; |
current := X; |
|
{переадресування вказівника current на елемент Х} |
|
||
|
|
|
|
|
|
|
{------------------------------------ |
|
|
процедура створення списку --------------------------------------- |
} |
||
procedure St_spisok(var l:spisok); |
{добавленням елементів до кінця списку} |
|
||||
var p:spisok; |
n : integer; |
|
|
|
||
begin |
|
|
|
|
|
|
|
l:=nil; |
|
{початкова ініціалізація списку} |
|
||
|
writeln('Введіть список цілих чисел. 999 - кінець:'); |
|
||||
|
readln(n); |
|
|
|
|
|
|
AddElemK(l, n); |
{створення першого елемента списку} |
|
|||
|
readln(n); |
|
|
|
|
|
|
while n<>999 do |
|
|
|
|
|
|
begin |
|
|
|
|
|
|
p := l; |
|
|
|
{ p – вказівник для руху по списку} |
|
|
while |
p^.next<>Nil |
do |
{пошук останнього елемента списку} |
|
|
|
p := p^.next; |
|
|
|
|
|
|
AddElemK(p, n); |
|
{добавлення елемента до кінця списку} |
|
||
|
readln(n); |
|
|
|
|
|
end; |
end; |
|
|
|
|
|
|
|
|
|
|
|
|
{--------------------------------- |
|
|
процедура виводить і знищує список -------------------------------- |
} |
||
procedure Vivod(var l:spisok); |
|
|
|
|||
var p:spisok; n:integer; |
|
|
|
|
||
begin |
|
|
|
|
|
|
|
n:=1; |
|
|
{початковий номер елемента списку} |
|
|
|
while l<>nil do |
|
{переглядаємо список} |
|
||
|
begin |
|
|
|
|
|
p:=l;
if n mod 2=0 then {числа на парному місці виводимо на екран} write(p^.data:10);
inc(n);
l:=l^.next;
97
dispose(p); |
{вилучаємо елемент списку} |
|
end; |
|
|
end; |
|
|
{--------------------------------------------- |
основна програма -------------------------------------------- |
} |
begin |
|
|
St_Spisok(l); |
|
|
Vivod(l); |
|
|
readkey; |
|
|
end. |
|
|
|
☺ Завдання для виконання |
|
|
Основний рівень |
|
1.Створити список, який містить інформацію про користувачів телефоном. (поля: прізвище, номер телефону, заборгованість). Знайти записи про клієнтів, борг яких перевищує деякий максимум. Вивести ці записи на екран та вилучити зі списку.
2.Створити список, який містить інформацію про користувачів водогоном (поля: прізвище, адреса, заборгованість). Перевірити список на наявність клієнтів, борг яких перевищує деякий мінімум. Вивести ці записи на екран та вилучити зі списку.
3.Створити список, який містить інформацію про пасажирів автобуса (поля: прізвище, номер місця, пункт призначення). Знайти інформацію про пасажирів, які прибули в певний пункт призначення. Вивести її на екран та вилучити відповідні записи зі списку.
4.Дано список слів. Написати програму, яка друкує текст із перших букв усіх слів списку.
5.Створити список, який містить інформацію про жителів міста. (поля: прізвище, адреса, вік). Перевірити список на наявність громадян, вік яких перевищує певний рівень. Вивести ці записи на екран та вилучити зі списку.
6.Створити список цілих чисел. Вивести на екран усі числа, які більші певного заданого числа, і вилучити їх зі списку.
7.Створити список цілих чисел. Вивести на екран усі числа, які менші певного заданого числа, і вилучити їх зі списку.
8.Створити список слів. Вивести на екран усі слова, які довші певної заданої довжини, і вилучити їх зі списку.
9.Створити список слів. Вивести на екран всі слова, які коротші певної заданої довжини, і вилучити їх зі списку.
10.Створити список слів. Замінити всі входження слова s1 на s2.
11.Створити список, який містить інформацію про користувачів телефоном. (поля: прізвище, номер телефону, заборгованість). Перевірити, чи є у списку дублікати записів про телефонних абонентів, та вилучити їх.
12.Перенести на початок списку його останній елемент.
13.Роздрукувати список у зворотному порядку.
14.Обчислити середнє арифметичне елементів списку цілих чисел і вилучити всі елементи більші нього.
15.Обчислити середнє арифметичне елементів списку цілих чисел і вилучити всі елементи менші нього.
98
Підвищений рівень
1.Дано три списки L, L1, L2 цілих чисел. Створити процедуру, яка у списку L заміняє перше входження списку L1 (якщо таке є) на список L2.
2.Створити програму, яка зі списків слів L1, L2 формує новий список L, заносячи до нього по одному разу ті слова, що містяться хоча б в одному зі списків L1 і L2.
3.Подвоїти входження кожного елемента списку. Елементи, які входять до списку більше двох разів, вилучити.
4.Файл містить дані: прізвище, адреса, телефон, зарплата. Відсортувати його за зростанням зарплати, використавши список. Прізвищами людей із однаковою зарплатою відсортувати в алфавітному порядку.
5.Файл містить дані: прізвище, адреса, телефон. Відсортувати прізвищами в алфавітному порядку, використавши список, а при наявності однакових прізвищ відсортувати адреси теж в алфавітному порядку. Здійснити пошук абонента за номером телефону.
6.Дано два списки цілих чисел. Утворити один список, відсортований за зростанням.
7.Дано два списки, елементами яких є слова. Утворити один список, відсортований від “Я” до “А”.
8.Дано два списки цілих чисел. Створити список, елементами якого є числа, які входять одночасно до обох списків.
9.Дано два списки цілих чисел. Створити список, елементами якого є числа, які входять до першого списку і не входять до другого.
10.(Лічилка) n дітей розташовуються по кругу. Почавши відлік від першого, вилучають кожного k-го, замикаючи круг після кожного вилучення. Написати програму, яка за даними n і k виводить номери дітей у тому порядку, за якими вони вилучалися із круга.
11. Многочлен P(x) = a n xn +a n−1xn−1 +... +a1x +a0 можна представити у вигляді списку.
P |
|
n |
an |
|
|
n-1 |
an-1 |
|
. . . |
0 |
a0 |
Nil |
|
|
|
|
|
|
|
|
|
|
|
|
Якщо аі = 0, то відповідний запис не включається до списку. Описати тип даних, що відповідає такому представленню многочленів, і створити логічну функцію дорівнює(Р,Q), яка перевіряє рівність многочленів Р і Q.
12. Многочлен P(x) = a n xn +a n−1xn−1 +... +a1x +a0 можна представити у вигляді списку.
P |
|
n |
an |
|
|
n-1 |
an-1 |
|
. . . |
0 |
a0 |
Nil |
|
|
|
|
|
|
|
|
|
|
|
|
Якщо аі = 0, то відповідний запис не включається до списку. Описати тип даних, що відповідає такому представленню многочленів, і створити логічну функцію знач(Р,Х), яка обчислює значення многочлена Р в цілочисловій точці Х.
13. Многочлен P(x) = a n xn +a n−1xn−1 +... +a1x +a0 можна представити у вигляді списку.
P |
|
n |
an |
|
|
n-1 |
an-1 |
|
. . . |
0 |
a0 |
Nil |
|
|
|
|
|
|
|
|
|
|
|
|
Якщо аі = 0, то відповідний запис не включається до списку. Описати тип даних, що відповідає такому представленню многочленів, і створити процедуру сум(Р,Q,R), яка обчислює многочлен R, який дорівнює сумі многочленів Р і Q.
14. Многочлен P(x) = a n xn +a n−1xn−1 +... +a1x +a0 можна представити у вигляді списку.
P |
|
n |
an |
|
|
n-1 |
an-1 |
|
. . . |
0 |
a0 |
Nil |
|
|
|
|
|
|
|
|
|
|
|
|
Якщо аі = 0, то відповідний запис не включається до списку. Описати тип даних, що відповідає такому представленню многочленів, і створити процедуру добуток(Р,Q,R), яка обчислює многочлен R, який дорівнює добутку многочленів Р і Q.
99
15. Многочлен P(x) = a n xn +a n−1xn−1 +... +a1x +a0 можна представити у вигляді списку.
P |
|
n |
an |
|
|
n-1 |
an-1 |
|
. . . |
0 |
a0 |
Nil |
|
|
|
|
|
|
|
|
|
|
|
|
Якщо аі = 0, то відповідний запис не включається до списку. Описати тип даних, що відповідає такому представленню многочленів, і створити процедуру діф(Р,Q), яка обчислює многочлен Q, що є похідною многочлена Р.
Питання для самоконтролю
1.Що таке динамічна пам’ять? Для чого вона служить?
2.Що таке вказівник?
3.Які ви знаєте види вказівників?
4.Які змінні називаються динамічними?
5.Назвіть основні операції над вказівниками.
6.Що таке список?
7.Назвіть різновиди лінійних списків.
8.Дайте означення однозв’язного лінійного (циклічного) списку.
9.Напишіть процедуру створення списку.
10.Які основні операції над списками ви знаєте?
11.Серед наведених нижче дій, для p:^integer; g:^integer знайдіть неправильні:
a) p:=5; |
b) g:=p |
c) g^:=p |
d) p^:=g^ |
e) p^:=g |
f) g:=5 |
g) DISPOSE(g); g:=p; |
|
12.Для чого служить процедура DISPOSE(Р)?
13.Для чого служить процедура NEW(Р)?
14.Які засоби має система Turbo Pascal для роботи з динамічною пам’яттю?
100
