Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

g30zAZLYUG

.pdf
Скачиваний:
3
Добавлен:
15.04.2023
Размер:
1.89 Mб
Скачать

Здесь type – тип элементов, помещаемых в очередь, а name – имя очереди. Операции, определённые над очередью, реализуются следующими

функциями:

push() – добавить элемент в хвост;

pop() – удалить первый элемент;

size() – размер очереди;

empty() – возвращает булево значение true, если очередь пуста;

front() – получить первый (головной) элемент;

back() – получить последний (хвостовой) элемент. Рассмотрим пример работы с контейнером queue (очередь).

5.1.Создайте новый проект (см. пункты 1.1–1.7 этой лабораторной работы; разумеется, надо задать другое имя проекта).

5.2.В раздел подключений добавьте ещё один include:

#include <queue>

5.3. В начало процедуры main добавьте объявление очереди. Дадим ей имя proba. И пусть в ней хранятся целые числа:

queue <int> proba;

5.4. С помощью функции push положите в очередь несколько целых чисел:

proba.push(20);

proba.push(30);

proba.push(40);

proba.push(50);

5.5. Распечатываем содержимое очереди до тех пор, пока она не опустеет:

while (!(proba.empty())) // Пока очередь не пуста

{

cout<<proba.front() << "\n"; // Печатаем головной элемент

proba.pop();

// Удаляем головной элемент

};

 

Запустите программу на выполнение. Обратите внимание на тот факт, что элементы выводятся на печать в том же порядке, что и вводятся.

41

Контрольные вопросы

1.Что означает применительно к линейному списку понятие «структура последовательного доступа»?

2.Какие разновидности линейных списков Вам известны?

3.Что представляет собой структура данных стек?

4.Перечислите операции, определённые над стеком.

5.Какие способы организации стека Вы знаете?

6.Что представляет собой структура данных очередь?

7.Перечислите операции, определенные над очередью.

8.Какие способы организации очереди Вы знаете?

9.Что такое указатель?

10.Какие действия выполняет предназначенный для работы с динамическими переменными оператор new?

11.Какую работу выполняет предназначенный для работы с динамическими переменными оператор free?

12.Как с помощью оператора switch организовать пользовательское меню для консольного приложения?

42

Лабораторная работа № 3. Линейные списки (окончание) – список с двумя связями

Список с двумя связями удобно использовать для представления упорядоченного множества элементов переменной длины, для которого необходимо часто выполнять включение и удаление элементов не только в голове или хвосте, как для очереди или стека, но и в произвольных местах списка с сохранением порядка.

Каждый элемент списка с двумя связями помимо некоторой содержательной информации должен включать две ссылки: как на предыдущий элемент, так и на последующий элемент этого списка (рисунок 3.1).

Рис. 3.1. Цепное представление списка с двумя связями.

Над списком с двумя связями определены следующие операции:

создать пустой список;

вставить элемент на своё место − в соответствии с заданным правилом упорядочения элементов;

удалить заданный элемент;

получить первый (начальный) элемент;

получить элемент, следующий за данным элементом;

проверка: является ли список пустым?

п. 1. Реализация списка с двумя связями с помощью указателей

Каждый элемент списка с двумя связями представляет собой запись, как минимум, с тремя полями, а именно:

поле (хотя бы одно) той смысловой информации, ради которой, собственно, мы и организуем список;

поле указателя на соседа слева;

поле указателя на соседа справа.

Для простоты в этом учебном примере будем считать, что полей ровно три, причём первое поле (поле смысловой информации) целочисленное. Добавлять элементы будем таким образом, чтобы упорядочить список в порядке возрастания элементов, хранящихся в первом (числовом) поле.

1.1. Создайте новый проект, отработав пункты 1.1−1.7 лабораторной работы № 2 (разумеется, надо задать другое имя проекта, например, lab_03).

43

1.2.После строки: using namespace std;

вобласти объявления глобальных типов и переменных вставьте объявление нового типа данных (с помощью уже известного Вам оператора typedef):

typedef struct List_2

{

 

int

data; // поле целочисленных данных

struct List_2

*lptr; // указатель на соседа слева (от англ. left pointer)

struct List_2

*rptr; // указатель на соседа справа (от англ. right pointer)

}NODE;

1.3.Сразу под объявлением нового типа NODE задайте объявление двух указателей на запись этого типа:

NODE *pbegin, *pend;

В указателе pbegin будет храниться адрес начального элемента, в указателе pend – адрес конечного элемента.

1.4. Непосредственно перед процедурой main вставьте заготовки для четырёх процедур:

вывода содержимого списка на экран;

поиска в списке элемента с заданным числовым полем;

добавления элемента в список;

удаления элемента из списка.

void Print_List_2()

{

NODE *p1;

cout << "*****Print_List_2*****\n";

};

NODE* Search_List_2(int number)

{

NODE *pnumber;

cout << "*****Search_List_2*****\n";

};

void Add_List_2(int number)

{

NODE *p1, *pleft, *pright;

cout << "*****Add_List_2*****\n";

};

void Del_List_2(NODE *&pnumber)

{

NODE *p1, *pleft, *pright, *pbuf; cout << "*****Del_List_2*****\n";

};

44

Обратите внимание на следующие обстоятельства:

1)в каждой процедуре объявлены локальные переменные, представляющие собой указатели на запись типа NODE. Эти указатели будут служить для вспомогательных целей. Их действие ограничено рамками той процедуры, в которой они объявлены;

2)процедура поиска имеет в объявлении не «void», а «NODE*», т.к. возвращает адрес элемента с заданным значением number числового поля (если элемент не найден, этот указатель обнулится);

3)у всех процедур, кроме процедуры печати, имеется передаваемый параметр.

Упроцедуры поиска входной параметр number – это число, по которому производится поиск: процедура вернёт адрес того элемента, у которого значение в поле data совпадёт со значением переменной number.

Упроцедуры добавления входной параметр number – это число, которое надо добавить в список.

Упроцедуры удаления входной параметр: pnumber – это адрес элемента, который надо удалить. Поскольку в процедуру Del_List_2 передаётся значение указателя pnumber, то в описании передаваемого параметра присутствует символ «&»:

NODE *&pnumber.

1.5.В процедуре main с помощью оператора выбора switch организуйте пользовательское меню (если забыли, как это делается, загляните в лабораторную работу № 2, пункт 2.3; после копирования не забудьте поменять названия процедур и слово «стек» заменить словом «список»). Пе-

редаваемые параметры в вызове процедур пока не указывайте.

1.6.Чтобы создать пустой, не содержащий ни одного элемента, список, надо обнулить указатели на начальный и конечный элементы. Для этого в начало процедуры main добавьте оператор:

pbegin=pend=NULL;

// Создание пустого списка

 

 

1.7. Будем считывать с консоли значение добавляемого (или удаляемого) числа, затем с помощью процедуры поиска проверять, содержится ли введённое число в списке, а уж потом вызывать процедуру добавления (или удаления) элемента.

1.7.1. В начале процедуры main объявите целочисленную переменную k и указатель pk на запись типа NODE:

int k; NODE *pk; pk = NULL;

1.7.2. В теле оператора выбора switch замените фрагмент case 0: … break;

следующими строками:

45

case 0: cout << "Введите добавляемое число\n"; cin >> k;

cin.ignore();

pk = Search_List_2(k);

// Добавляем число k в список, если его ещё нет в списке if (pk == NULL) Add_List_2(k);

break;

Поясню. Введённое числовое значение k передаём в процедуру поиска Search_List_2. Процедура возвращает адрес pk элемента с числовым полем k. Если адрес нулевой, то такого элемента в списке ещё нет. И тогда вызываем процедуру добавления, передавая в неё само число k. В противном случае ничего не делаем.

1.7.3. Аналогично, фрагмент case 1: … break;

замените следующими строками:

case 1: cout << "Введите удаляемое число\n"; cin >> k;

cin.ignore();

pk = Search_List_2(k);

// Удаляем число k, если оно в списке присутствует if (pk != NULL) Del_List_2(pk);

break;

Здесь, наоборот, процедуру удаления вызываем лишь в том случае, когда удаляемый элемент обнаружен в списке, т.е. его адрес не нулевой. И передаём в неё не число k, а адрес удаляемого элемента.

1.8. Процедура печати непустого списка практически не отличается от рассмотренной в лабораторной работе № 2 процедуры печати очереди:

void Print_List_2()

{

NODE *p1;

cout << "*****Print_List_2*****\n";

//Встаём на начало списка p1=pbegin;

//Делаем, пока не дошли до конца while (p1 != NULL)

{

//Печатаем поле данных элемента, расположенного по адресу p1 cout << p1->data << "\n";

//Сдвигаемся к соседу справа

p1 = p1->rptr;

}

};

46

1.9. Займёмся процедурой поиска. Чтобы найти элемент с заданным значением number числового поля, встаём на начало списка (если он не пустой) и движемся вправо.

NODE* Search_List_2(int number)

{

NODE *pnumber; pnumber = NULL;

if (pbegin != NULL) // Работаем, если список не пуст

{

//Встаём на начало списка pnumber = pbegin;

//По значению числового поля ищем адрес элемента while ((pnumber != NULL) && (pnumber->data != number))

{pnumber = pnumber->rptr;};

}

return(pnumber);

};

Условие продолжения цикла представляет собой конъюнкцию двух условий:

pnumber != NULL, т.е. не достигнут конец списка;

pnumber->data != number, т.е. значение числового поля текущего элемента списка не совпадает с заданным числом number.

Когда хотя бы одно из двух условий нарушится, цикл завершит свою работу. Если нарушится первое условие, то pnumber примет значение NULL; это означает, что мы дошли до конца списка, так и не найдя элемент, числовое поле которого равно данному значению k. Такого элемента в списке нет. Если нарушится второе условие, то требуемый элемент найден, причём он расположен по адресу pnumber.

1.10. С процедурой Add_List_2(), добавляющей элемент в список так, чтобы он был упорядочен по первому полю, придётся повозиться, т.к. здесь существует несколько возможностей, каждая из которых требует своего способа обработки.

1.10.1. Начало не нуждается в особых комментариях, так как аналогично началу процедуры добавления элемента в очередь или стек (см. лабораторную работу № 2):

void Add_List_2(int number)

{

NODE *p1, *pleft, *pright;

//Выделяем место в памяти под новую переменную типа NODE p1 = new NODE;

//По адресу p1 в поле данных помещаем добавляемое число

p1->data = number;

47

1.10.2. Особый случай – это когда элемент добавляется в пустой, не содержащий ни одного элемента, список. Признак пустого списка − равенство нулю указателя на начальный элемент. В этом случае у единственного добавленного элемента нет соседей; поэтому указатели на соседа справа и слева полагаем равными нулю. Кроме того, переопределяем указатели на начало и конец списка, а именно: и начальный, и конечный элемент – это тот, который только что размещён по адресу p1. Добавьте соответствующие строки:

if (pbegin == NULL)

{p1->lptr = NULL; // Соседа слева нет; указатель на него равен нулю p1->rptr = NULL; // Соседа справа нет; указатель на него равен нулю

//Начальный элемент теперь тот, который только что добавлен pbegin = p1;

//Конечный элемент теперь тот, который только что добавлен

pend = p1;

}

else

{

// Добавление в непустой список

}

Print_List_2();

}

Как видите, в блоке else пока лишь заглушка в виде комментария. Займёмся содержимым этого блока.

1.10.3. Итак, элемент добавляется в непустой список. Здесь тоже имеется несколько принципиально различных возможностей. Первая из них – вставка перед начальным элементом. Признак такой ситуации – добавляемое число не больше числа в поле данных начального элемента:

p1->data <= pbegin->data

Например, в списке уже хранятся числа 36, 48, 67, 95. Необходимо добавить число 23. В этом случае (рис. 3.2):

Рис. 3.2. Вставка нового элемента перед начальным элементом

у начального элемента появляется сосед слева: pbegin->lptr = p1;

у вновь добавленного элемента – сосед справа: p1->rptr = pbegin;

новый элемент становится начальным: pbegin = p1;

48

Замените строку

//Добавление в непустой список следующим кодом:

//Добавление в непустой список

if (p1->data <= pbegin->data) // вставка ПЕРЕД начальным элементом

{

//У начального элемента появился сосед слева pbegin->lptr = p1;

//У вновь добавленного элемента появился сосед справа p1->rptr = pbegin;

//У вновь добавленного элемента нет соседа слева

p1->lptr = NULL;

// Начальным становится новый элемент pbegin = p1;

}

else

{

// Добавление НЕ перед начальным

}

И снова в блоке else заглушка в виде комментария. Переходим к рассмотрению этого блока.

1.10.4. Итак, элемент добавляется не перед начальным. Остаются ещё две принципиально различных возможности. Первая из них – вставка после конечного элемента. Признак такой ситуации – добавляемое число не меньше числа в поле данных конечного элемента:

p1->data >= pend->data

Например, в списке уже хранятся числа 36, 48, 67, 95. Необходимо добавить число 98. В этом случае (рис. 3.3):

Рис. 3.3. Вставка нового элемента после конечного элемента

у конечно элемента появляется сосед справа: pend->rptr = p1;

у вновь добавленного элемента – сосед слева: p1->lptr = pend;

новый элемент становится конечным: pend = p1;

49

Замените строку

//Добавление НЕ перед начальным следующим кодом:

//Добавление НЕ перед начальным

if (p1->data >= pend->data) // Добавление ПОСЛЕ конечного элемента

{

//У конечного элемента появился сосед справа pend->rptr = p1;

//У вновь добавленного элемента появился сосед слева p1->lptr = pend;

//У вновь добавленного элемента нет соседа справа

p1->rptr = NULL;

// Конечным становится новый элемент pend = p1;

}

else

{

// Добавление в середину списка

}

1.10.5. Осталась последняя возможность. Новый элемент вставляется где-то между начальным и конечным. Назовём этот случай «вставка в середину списка». В смысле обработки этот случай самый трудоёмкий, т.к. сначала надо найти место для вставки, а уж потом эту самую вставку производить.

Параллельно с дальнейшими объяснениями будем рассматривать поясняющий пример. Пусть в списке уже хранятся числа

23, 36, 48, 67, 95.

Необходимо добавить число 50. Очевидно, элемент с числовым полем 50 должен занять своё место элементами 48 и 67. Обозначим адрес элемента 48 через pleft, а адрес элемента 67 через pright.

Прежде чем производить вставку, надо определить адреса pleft и pright. Делается это так. Сначала ищем pright. Для этого встаём на начало списка и двигаемся по списку вправо до тех пор, пока значение числового поля у вновь добавленного элемента больше значения числового поля у текущего элемента списка:

pright = pbegin;

while (p1->data > pright->data) {pright = pright->rptr};

В нашем примере условие цикла нарушится при проверке истинности неравенства:

50 > 67.

50

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]