g30zAZLYUG
.pdf2.8. Измените текст процедуры печати непустого стека на следую-
щий:
void Print_LIFO()
{
cout << "*****Print_LIFO*****\n";
p1=ptop; // Начинаем с верхушки стека
while (p1 != NULL) // Делаем, пока не дошли до самого низа
{
// Печатаем поле данных элемента, расположенного по адресу p1 cout << p1->data << "\n";
// Сдвигаемся к следующему элементу p1 = p1->ptr;
}
};
Хотя текст процедуры снабжён подробными комментариями, поясню суть её работы на простом примере. Предположим, что у Вас имеется стек, в который помещены три элемента. Назовём их условно: верхний, средний и нижний. Структура такого стека показана на рис. 2.3.
31
Рис. 2.3.
Структура стека
В результате выполнения оператора p1 = ptop;
в указатель p1 засылается адрес верхнего элемента стека. Начинает работать цикл. Оператор cout печатает содержимое поля данных верхнего элемента, после че-
го в результате выполнения оператора p1 = p1->ptr;
впеременную p1 попадает то, что хранится у верхнего элемента в поле указателя, т.е. адрес среднего элемента. Таким образом, мы перешли к среднему элементу.
Следующий шаг цикла. Оператор cout печатает содержимое поля данных среднего элемента, после чего в результате выполнения оператора
p1 = p1->ptr;
впеременную p1 попадает то, что хранится у среднего элемента в поле указателя, т.е. адрес нижнего элемента. Тем самым, мы перешли к нижнему элементу.
Следующий шаг цикла. Оператор cout печатает содержимое поля данных нижнего элемента, после чего в результате выполнения оператора
p1 = p1->ptr;
впеременную p1 попадает то, что хранится у нижнего элемента в поле указателя, т.е. нулевой указатель NULL. Условие продолжения цикла нарушено, и цикл завершает свою работу.
2.9. Приступим к разработке процедуры Add_LIFO, добавляющей элемент в стек.
void Add_LIFO(int number)
{
cout << "***** Add_LIFO*****\n";
//Выделяем место в памяти под новую переменную типа NODE;
//адрес выделенного места помещаем в переменную p1
p1=new NODE;
//По адресу p1 в поле данных помещаем введённое число p1->data=number;
//По адресу p1 в поле указателя помещаем адрес прежней верхушки p1->ptr=ptop;
//Переопределяем верхушку, т.к. верхним стал вновь введённый элемент ptop=p1;
// Выводим стек на экран
Print_LIFO();
};
32
Как видите, в этой процедуре выполняются следующие действия:
c помощью оператора new выделяется место в памяти под новую переменную, имеющую тип NODE. Адрес выделенного места помещается в указатель p1;
в поле данных нового элемента помещается значение переменной number;
в поле указателя нового элемента заносится адрес того элемента, который «под ним», т.е. адрес прежней верхушки;
поскольку теперь на самом верху стека новый, только что добавленный, элемент, то именно его адрес – это адрес верхушки. Переопределяется верхушка;
стек выводится на печать.
Запустите программу на выполнение. Используя пользовательское меню, добавьте в стек несколько новых элементов. Обратите внимание на тот факт, что элемент, попавший в стек последним, выводится на печать первым.
2.10. Наконец, процедура Del_LIFO, удаляющая верхушку стека. Если стек не пуст (есть что удалять), то переопределяем верхушку стека (адрес новой верхушки – это то, что хранится у старой верхушки в поле указателя), после чего очищаем место в памяти, занимаемое прежней верхушкой.
void Del_LIFO()
{// Адрес удаляемого верхнего элемента
//копируем во вспомогательную переменную p1 p1 = ptop;
//Переопределяем верхний элемент
if (ptop != NULL) ptop = ptop->ptr;
//Очищаем место, занимаемое удалённым элементом free(p1);
//Выводим стек на экран
Print_LIFO();
};
Запустите программу на выполнение. Используя пользовательское меню, добавьте в стек несколько новых элементов, а затем их поочерёдно удалите.
ЗАДАНИЕ № 1. Хранение набора записей в виде стека
Модифицируйте созданную программу так, чтобы в стеке хранились не целые числа, а набор данных из Вашего варианта (см. задание № 1 лабораторной работы № 1).
33
В структуре LIFO у Вас будет не два, а пять полей: четыре поля из Вашего набора данных плюс поле указателя.
Предусмотрите ввод элементов стека не с консоли, а из входного текстового файла (того самого, который Вы подготовили для выполнения задания № 1 лабораторной работы № 1).
Подсказка. В процедуре main выше метки label_1 организуйте выполнение следующих действий:
объявите буферную переменную типа NODE. Например, так:
NODE buff;
объявите файловую переменную f и откройте файл для чтения;
в цикле считывайте из файла очередную запись и помещайте её в переменную buff, после чего вызывайте процедуру Add_LIFO, передавая в неё не целочисленное значение, а запись buff. Например, так:
while (!f.eof()) |
// пока не будет достигнут конец файла |
{ |
|
//Читаем строку из файла в переменную buff f >> buff.surname >> buff.name >> buff.ball;
//Передаём запись в процедуру добавления элемента
Add_LIFO(buff);
};
в самой процедуре Add_LIFO заполняйте поля новой записи, считывая их с переданной переменной:
void Add_LIFO(NODE b)
{
cout << "***** Add_LIFO*****\n";
//Выделяем место в памяти под новую переменную типа NODE p1=new NODE;
//Оценка
p1->ball=b.ball; // Фамилия
strcpy(p1->surname, b.surname); // Имя
strcpy(p1->name, b.name);
//По адресу p1 в поле указателя помещаем адрес прежней верхушки p1->ptr=ptop;
//Переопределяем верхушку, т.к. верхним стал вновь введённый элемент ptop=p1;
};
34
после окончания цикла в процедуре main распечатайте содержимое стека (с помощью модифицированной под Ваши данные процедуры
PRINT_LIFO).
Фрагмент добавления элементов посредством пользовательского меню можете закомментировать.
Работающую версию программы, а также программный код, предъявите преподавателю.
п. 3. STL контейнер stack (стек)
Способ организации стека, представленный в предыдущем пункте, универсален в том смысле, что аналогичным образом можно организовать стек и в другом языке программирования (например, Pascal), и в другой среде программирования (например, Builder, Delphi).
Кроме того, умение работать с динамическими данными посредством указателей позволяет разрабатывать эффективные программные приложения, имеющие высокое быстродействие и рационально расходующие компьютерную память. Что говорит об отличном профессиональном уровне программиста.
Однако Microsoft Visual Studio C++ предлагает разработчикам ПО ещё один способ организации стека – с помощью STL контейнера stack. Вся информация об этой структуре хранится в библиотеке
<stack>.
Чтобы создать новый стек, его надо объявить в коде программы следующим образом:
stack <type> name;
Здесь type – тип элементов, помещаемых в стек, а name – имя стека.
Операции, определённые над стеком, реализуются с помощью следующих функций:
push() – добавить элемент;
pop() – удалить верхний элемент;
top() – получить верхний элемент;
size() – размер стека;
empty() – возвращает булево значение true, если стек пуст. Рассмотрим пример работы с контейнером stack.
3.1.Создайте новый проект (см. пункты 1.1 – 1.7 этой лабораторной работы; разумеется, надо задать другое имя проекта, например, lab_02_2).
3.2.В раздел подключений библиотек добавьте ещё один include:
#include <stack>
35
3.3. В начало процедуры main добавьте объявление стека. Дадим ему имя proba. И пусть в нем хранятся целые числа:
stack <int> proba;
3.4. С помощью функции push положите в стек несколько целых чи-
сел:
proba.push(20); proba.push(30); proba.push(40); proba.push(50);
3.5. Распечатываем содержимое стека до тех пор, пока он не опусте-
ет:
while (!(proba.empty())) |
// Пока стек не пуст |
|
{ |
|
|
cout<<proba.top() << "\n"; |
// Печатаем верхушку |
|
proba.pop(); |
// Удаляем верхушку |
|
}; |
|
|
|
|
|
Запустите программу на выполнение. Обратите внимание на тот факт, что элемент, попавший в стек последним, выводится на печать первым.
п. 4. Очередь. Реализация очереди с помощью указателей
4.1.Закомментируйте или удалите последние добавленные строки, вернувшись к состоянию, описанному в пункте 1.6.
4.2.Как и в случае со стеком, каждый элемент очереди представляет собой запись, как минимум, с двумя полями, а именно:
поле (хотя бы одно) той смысловой информации, ради которой, собственно, мы и организуем очередь и
поле указателя.
Для простоты будем считать, что полей ровно два, причём первое
поле (поле смысловой информации) целочисленное.
В области объявления глобальных типов и переменных (после строки “using namespace std;”) вставьте объявление нового типа данных:
typedef struct FIFO
{
int data; // поле целочисленных данных struct FIFO *ptr; // указатель на запись типа NODE
}NODE;
4.3.Сразу под объявлением нового типа NODE задайте объявление трёх указателей на запись этого типа:
36
NODE *phead, *ptail, *p1;
В указателе phead будет храниться адрес головного элемента, в указателе ptail – адрес хвостового элемента, а указатель p1 понадобится нам в качестве вспомогательной переменной. Мы объявили их в области объявления глобальных переменных для того, чтобы все процедуры имели к ним доступ.
4.4. Чтобы создать пустую, не содержащую ни одного элемента, очередь, надо обнулить указатели на хвостовой и головной элементы. Для этого в начало процедуры main добавьте оператор:
phead=ptail=NULL; // Создание пустой очереди
4.5. Внесём информацию о добавляемом в стек числе. В начале процедуры main объявите целочисленную переменную k:
int k;
4.6.Аналогично пункту 2.6. вставьте три так называемые заглушки для будущих процедур:
Print_FIFO – вывода содержимого очереди на экран,
Add_FIFO – добавления элемента в очередь,
Del_FIFO – удаления элемента из очереди.
4.7.Аналогично пункту 2.7 организуйте пользовательское меню, заменив везде слово «стек» на слово «очередь», а сочетание «LIFO» на
«FIFO».
4.8.Измените текст процедуры печати непустой очереди на следую-
щий:
void Print_FIFO()
{
cout << "*****Print_FIFO*****\n";
p1 = phead; // Встаём на начало очереди
while (p1 != NULL) // Пока не дошли до конца очереди
{
//Печатаем поле данных элемента, расположенного по адресу p1 cout << p1->data << "\n";
//Сдвигаемся к следующему элементу
p1 = p1->ptr;
}
};
37
Поясню суть работы процедуры на простом примере. Предположим, что у Вас имеется очередь, в которую помещены три элемента. Назовём их условно: головной, средний и хвостовой. Структура такой очереди показана на рисунке 2.4.
Рис. 2.4. Структура очереди
В результате выполнения оператора p1 = phead;
в указатель p1 засылается адрес головного элемента.
Начинает работать цикл. Оператор cout печатает содержимое поля данных головного элемента, после чего в результате выполнения оператора
p1 = p1->ptr;
в переменную p1 попадает то, что хранится у головного элемента в поле указателя, т.е. адрес среднего элемента. Таким образом, мы перешли к среднему элементу.
Следующий шаг цикла. Оператор cout печатает содержимое поля данных среднего элемента, после чего в результате выполнения оператора
p1 = p1->ptr;
в переменную p1 попадает то, что хранится у среднего элемента в поле указателя, т.е. адрес хвостового элемента. Тем самым, мы перешли к хвостовому элементу.
Следующий шаг цикла. Оператор cout печатает содержимое поля данных хвостового элемента, после чего в результате выполнения оператора
p1 = p1->ptr;
в переменную p1 попадает то, что хранится у хвостового элемента в поле указателя, т.е. нулевой указатель NULL. Условие продолжения цикла нарушено, и цикл завершает свою работу.
38
4.8. Процедура Add_FIFO, добавляющая элемент в очередь.
void Add_FIFO(int number)
{
//cout << "***** Add_FIFO*****\n";
//Выделяем место в памяти под новую переменную типа NODE p1 = new NODE;
//По адресу p1 в поле данных помещаем введённое число k
p1->data = number;
//По адресу p1 в поле указателя помещаем ноль p1->ptr = NULL;
//Если вставка производится в пустую очередь,
//то новый элемент - головной,
//переопределяем указатель на голову очереди
//Иначе в поле указателя прежнего хвостового элемента
//помещаем адрес "новичка"
if (phead == NULL) {phead = p1;}
else
{ptail->ptr = p1;};
//Переопределяем хвостовой элемент,
//т.к. теперь последним стал вновь введённый элемент ptail = p1;
Print_FIFO(); |
// Печатаем очередь |
};
Текст процедуры снабжён подробными комментариями; поэтому в пояснениях остановлюсь только на принципиальном отличии этой процедуры от процедуры добавления элемента в стек.
Здесь отдельно надо обрабатывать ситуацию, когда вставка производится в пустую, не содержащую ни одного элемента, очередь. Если очередь пустая, то (см. пункт 4.6)
phead=ptail=NULL;
В этом случае обращение к ячейке памяти ptail->ptr
(т.е. к тому, что расположено по адресу ptail в поле указателя) привело бы к прерыванию программы.
Таким образом, если вновь добавленный элемент – единственный в очереди, то мы переопределяем указатель на голову очереди:
phead = p1;
В противном случае в поле указателя прежнего хвостового элемента помещаем адрес вновь введённого элемента:
ptail->ptr = p1;
39
Группа if-else заканчивается, после чего в обоих случаях переопределяем хвостовой элемент:
ptail = p1;
Запустите программу на выполнение. Используя пользовательское меню, добавьте в очередь несколько новых элементов. Обратите внимание на тот факт, что элементы выводятся на печать в том же порядке, что и вводятся.
4.9. Наконец, процедура Del_LIFO, удаляющая головной элемент. Она очень похожа на рассмотренную ранее процедуру удаления элемента из стека и в особых комментариях не нуждается.
void Del_FIFO()
{
// Адрес удаляемого головного элемента копируем
//во вспомогательную переменную p1 p1 = phead;
//Если очередь не пуста, переопределяем головной элемент if (phead != NULL) phead = phead->ptr;
//Очищаем место, занимаемое удалённым элементом free(p1);
//Выводим очередь на экран
Print_FIFO();
};
Запустите программу на выполнение. Используя пользовательское меню, добавьте в очередь несколько новых элементов, а затем их поочерёдно удалите.
ЗАДАНИЕ № 2. Хранение набора записей в виде очереди
Модифицируйте созданную программу так, чтобы в очереди хранились не целые числа, а набор данных из Вашего варианта (см. задание № 1 лабораторной работы № 1).
Предусмотрите ввод элементов очереди не с консоли, а из входного текстового файла – так же, как и в задании № 1 этой лабораторной работы.
п. 5. STL контейнер queue (очередь)
Microsoft Visual Studio C++ предлагает разработчикам ПО ещё один способ организацииочереди – с помощью STL контейнера queue. Вся информация об этой структуре хранится в библиотеке <queue>.
Чтобы создать новую очередь, её надо объявить в коде программы следующим образом:
queue <type> name;
40