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

g30zAZLYUG

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

Так как это неравенство ложное, цикл закончится, и в переменной pright сохранится адрес элемента с числовым полем 67.

Зная адрес pright, определим адрес pleft. Он хранится у элемента pright в поле левого указателя:

pleft = pright->lptr;

Вот теперь можно производить вставку. После вставки нового элемента должна получиться структура, изображённая на рисунке 3.4. Чтобы этого достичь, переопределяем указатели (см. стрелки на рис. 3.4):

у элемента 48 теперь сосед справа – 50: pleft->rptr = p1;

у элемента 67 теперь сосед слева – 50: pright->lptr = p1;

у элемента 50 сосед слева – 48: p1->lptr = pleft;

у элемента 50 сосед справа – 67: p1->rptr = pright;

Рис. 3.4. Вставка в середину списка

Замените в коде программы строку // Добавление в середину списка

на следующий фрагмент:

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

//Поиск адреса будущего соседа справа pright = pbegin;

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

//Определение адреса будущего соседа слева pleft = pright->lptr;

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

//У соседа справа в поле левого указателя заносим адрес нового элемента pright->lptr = p1;

//У нового элемента в поле левого указателя заносим адрес соседа слева p1->lptr = pleft;

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

51

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

Примечание. Так как мы выдвинули требование уникальности элементов, то каждое из добавленных чисел присутствует в списке один раз. Если, наоборот, в некоторой задаче число при повторном вводе должно в списке дублироваться, то достаточно просто закомментировать проверку наличия элемента перед вызовом процедуры добавления.

1.11. Настала очередь процедуры Del_List_2, удаляющей элементы из списка.

1.11.1. Итак, нам надо удалить из списка элемент, расположенный по адресу pnumber, причём pnumber != NULL (иначе процедура удаления не была бы вызвана). Сначала выясним адреса соседей этого элемента.

void Del_List_2(NODE *&pnumber)

{

NODE *pleft, *pright, *pbuf;

pleft = pnumber->lptr; // Адрес соседа слева pright = pnumber->rptr; // Адрес соседа справа

// Удаляем элемент (заглушка)

Print_List_2();

}

1.11.2. Как и при добавлении элемента, здесь возможны четыре различных ситуации, каждая из которых требует своего метода обработки. Перечислим их: у удаляемого элемента

нет соседей – ни слева, ни справа (это означает, что он единственный в списке);

нет соседа слева и есть сосед справа (это означает, что он начальный);

есть сосед слева и нет соседа справа (это означает, что он конечный);

есть оба соседа – и слева, и справа (это означает, что он где-то в середине списка)

Чтобы отразить все эти случаи в структуре процедуры удаления, замените строку

// Удаляем элемент (заглушка) следующим кодом:

52

// Копируем адрес удаляемого элемента в буферную переменную pbuf = pnumber;

if (pleft==NULL) // Нет соседа слева

{

if (pright==NULL) // Нет соседа справа

{

// Удаляемый элемент – единственный в списке

}

else

{

// Удаляемый элемент – начальный

}

}

else // Сосед слева присутствует

{

if (pright==NULL) // Нет соседа справа

{

// Удаляемый элемент – конечный

}

else

{

// Удаляемый элемент в середине списка

}

}

// Освобождаем место в памяти для дальнейшего использования free (pbuf);

В этой схеме представлены все четыре ситуации. Рассмотрим каждую из них.

111.3. Если удаляемый элемент – единственный в списке, то остаётся только обнулить указатели на начало и конец списка. С этой целью замените строку

// Удаляемый элемент – единственный в списке следующими строками:

// Удаляемый элемент – единственный в списке pbegin = NULL; // Обнуляем указатель на начало pend = NULL; // Обнуляем указатель на конец

1.11.4. Если удаляемый элемент – начальный, то надо переопределить начало списка: адрес нового начального элемента хранится в поле правого указателя у прежнего начального элемента. Замените строку

// Удаляемый элемент – начальный следующими строками:

53

// Удаляемый элемент – начальный

pbegin = pbegin->rptr; // Переопределяем начало

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

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

// Удаляемый элемент – конечный следующими строками:

// Удаляемый элемент – конечный

pend = pend->lptr; // Переопределяем конец

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

111.6. Наконец, если удаляемый элемент в середине списка, то надо переопределить указатели у его соседей.

Например, пусть до удаления элемента 50 ситуация такая:

Рис. 3.5. Удаляемый элемент – 50.

Чтобы удалить элемент 50, надо изменить указатели так, чтобы у элемента 48 указатель на соседа справа стал адресом элемента 67, а у элемента 67 указатель на соседа слева стал адресом элемента 48. Для этого замените строку

// Удаляемый элемент в середине списка следующими строками:

//Удаляемый элемент в середине списка

//У соседа слева в поле правого указателя вносим адрес соседа справа pleft->rptr = pright;

//У соседа справа в поле левого указателя вносим адрес соседа слева pright->lptr = pleft;

54

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

ЗАДАНИЕ № 1. Хранение набора записей в виде списка с двумя связями

Модифицируйте программу так, чтобы в списке хранились не целые числа, а набор данных из Вашего варианта (см. задание № 1 лабораторной работы № 1). Упорядочите список по числовому полю. Если в наборе данных Вашего варианта несколько числовых полей, то выберите любое из них.

Подсказка. Ваша структура List_2 будет содержать не три, а шесть полей: четыре поля из Вашего набора данных плюс два поля указателей.

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

Работающую версию программы, а также программный код, предъявите преподавателю.

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

1.Что представляет собой список с двумя связями?

2.Какие операции определены над списком с двумя связями?

3.Сколько полей, как минимум, должны иметь записи, помещаемые в список с двумя связями?

4.Каков механизм поиска элемента в списке с двумя связями?

5.Перечислите ситуации, которые должны быть учтены при добавлении элемента в список с двумя связями.

6.Какие действия следует произвести, чтобы:

запретить элементам в списке повторяться и, наоборот;

разрешить элементам в списке повторяться?

7.Какие ситуации должны быть учтены при удалении элемента из списка с двумя связями?

55

Лабораторная работа № 4. Сортировка

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

Пусть последовательность, которую необходимо отсортировать, представляет собой массив A, содержащий n записей – c 0-й по n–1-ю. Поле записи, по которому производится сортировка, будем называть ключе-

вым.

Обзор сортировок включением

Сортировка включением (путем вставок) состоит в следующем: вы-

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

Простейший метод, использующий этот принцип, метод простых вставок. Будем сравнивать по очереди ключ i-й записи с ключами записей: i 1-й, i 2-й, … до тех пор, пока не обнаружим, что i-ю запись следует вставить между записями j-й и j+1-й. Подвинув записи с j+1-й по i 1-ю на одну позицию вправо, поместим i-ю запись в позицию j+1.

Этот алгоритм обладает важным достоинством: он имеет наилучшую эффективность, если в начальном массиве уже установлен некоторый порядок.

Как средняя, так и максимальная временная сложность T(n) алгоритма составляет порядка n2, т.е.

T(n) = O(n2).

Такая низкая эффективность объясняется тем, что каждый элемент перемещается за один раз только на одну позицию: если m-й элемент массива должен быть перемещен в позицию 1, необходимо переместить на одну позицию вправо m 1 предшествующих элементов.

Усовершенствованная сортировка включением известна как сортировка Шелла. В 1959 году Д.Л. Шелл предложил вместо систематического включения элемента с индексом i в группу предшествующих ему элементов включать этот элемент в группу, содержащую элементы с индексами i h, i 2h, i 3h и т.д., где h некоторая натуральная постоянная. Таким образом, формируется массив, в котором h-серии элементов, отстоящих друг от друга на расстояние h, сортируются отдельно.

После того, как отсортированы непересекающиеся h-серии, процесс возобновляется с новым значением h* < h. Предварительная сортировка се-

56

рий с расстоянием h значительно ускоряет сортировку серий с расстоянием h*. Последний проход осуществляется со значением: h*…* = 1.

Для достаточно больших массивов результаты тестов позволяют рекомендовать последовательность Шелла:

h0 = 1,

hk+1 = 3∙hk +1,

k = 0, 1, 2…

Начать процесс следует с такого элемента этой последовательности, который является ближайшим к целой части числа n / 9, превосходящим это число.

Пример. Пусть сортируемый массив содержит n = 1000 элементов. Тогда [n / 9] = 111. Выпишем несколько элементов последовательности Шелла:

1, 4, 13, 40, 121, 364, …

Здесь число, ближайшее к 111, его превосходящее – это 121. Следовательно, при n = 1000 внешний цикл отработает пять раз: на первом шаге

h = 121, на втором шаге h* = 40, на третьем шаге h** = 13, на четвёртом шаге h*** = 4, на последнем, пятом шаге, h**** = 1.

Вместо последовательности Шелла можно использовать последовательность Кнута:

hk = 2k 1, i = 1, 2, 3…

В случае применения последовательности Кнута временная сложность алгоритма Шелла составляет O(n5/4).

Для больших значений n рекомендуется последовательность Седге-

вика:

 

9 2k

9 2k / 2

1,

если k чётное число,

hk

 

 

 

 

 

 

 

 

 

 

 

k

 

(k 1) / 2

 

 

 

 

 

2

6 2

1,

если k нечётное число.

 

8

 

 

 

В случае применения последовательности Седгевика алгоритм Шелла имеет временную сложность O(n7/6).

Ещё одной эффективной разновидностью сортировки включением является сортировка слиянием.

Вместо разделения элементов массива на h-серии разделим массив на две примерно равные части, рекурсивно упорядочим каждую из них, после чего эти упорядоченные части сольём. Под слиянием понимают объединение двух уже упорядоченных последовательностей P1 и P2 в одну упорядоченную последовательность P3.

Подробнее рассмотрим вспомогательную процедуру слияния, которая работает следующим образом: элементы последовательности P1 помещаются в стек S1, элементы P2 – в стек S2. Третий стек S3 служит для формирования P3. Первоначально он пуст. Затем осуществляется сравнение

57

верхних элементов в стеках S1 и S2. Наибольший из двух сравниваемых элементов переносится из своего стека в S3. Сравнения осуществляются до тех пор, пока один из стеков S1, S2 (или оба сразу) не опустеет. Если опустел один из этих стеков, то оставшиеся элементы из другого перегружаются в S3.

Как средняя, так и максимальная временная сложность алгоритма сортировки слиянием составляет O(n∙log(n)).

Обзор сортировок обменами

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

Простейший вид сортировки обменами, в которой сравниваются соседние элементы, получил название пузырьковая сортировка. Как сред-

няя, так и максимальная временная сложность пузырьковой сортировки составляет O(n2).

Эффективной разновидностью сортировки обменами является алго-

ритм Хоара, известный также как быстрая сортировка или сортировка сегментацией.

На первом этапе быстрой сортировки упорядочиваемый массив реорганизуется. Для этого с помощью генератора случайных чисел в массиве выбирается главный элемент. Значение ключевого поля этого элемента обозначим key. Затем все элементы с ключами, меньшими либо равными key, размещаются слева от главного элемента, а элементы с ключами, большими либо равными key, размещаются справа от него.

Затем для полной сортировки массива достаточно (второй этап):

а) больше не трогать главный элемент, который находится на своём месте;

б) рекурсивно отсортировать подмассив слева от главного элемента; в) рекурсивно отсортировать подмассив справа от главного элемента.

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

Вспомогательная процедура реорганизации массива относительно главного элемента работает следующим образом:

двигаясь от элемента с индексом first вправо, находят элемент с индексом f, ключ которого не меньше ключа главного элемента;

двигаясь от элемента с индексом last влево, находят элемент с индексом l, ключ которого не больше ключа главного элемента;

если оказалось, что элемент с индексом f расположен левее элемента с индексом l, то их меняют местами.

58

Дальше движение влево продолжают с позиции f+1, движение вправо с позиции l-1 до тех пор, пока не произойдет встреча, т.е. пока

f l.

Алгоритм Хоара имеет среднюю временную сложность O(n∙log(n)), тогда как максимальная сложность составляет O(n2).

Обзор сортировок извлечением

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

Простейший вид сортировки извлечением – это сортировка посредством простого выбора, которая заключается в следующем: на нулевом шаге находим элемент с минимальным ключом и меняем этот элемент местами с нулевым элементом. Перед i-м шагом элементы с 0-го по i 1-й уже стоят на своих местах. На i-м шаге ищем элемент с минимальным ключом среди элементов с i-го по n 1-й и меняем его местами с i-м элементом.

Как средняя, так и максимальная временная сложность этого алгоритма O(n2).

Уильямс и Флойд усовершенствовали сортировку извлечением, по-

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

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

Свойства сортирующего дерева

элемент с наибольшим ключом соответствует корню;

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

Хранят сортирующее дерево в виде массива по следующему правилу:

в корне дерева находится нулевой элемент массива;

если i-й элемент массива помещён в некоторую вершину V, то в левом потомке вершины V хранится элемент с индексом 2∙i+1, а в правом – элемент с индексом 2∙i+2.

Пример сортирующего дерева и соответствующего ему массива приведен на рисунке 4.1.

59

Индекс

Ключ

Индекс

Ключ

0

50

8

20

1

48

9

6

2

36

10

2

3

20

11

14

4

8

12

5

5

14

13

7

6

12

14

10

7

17

Рис. 4.1. Пример сортирующего дерева

 

 

 

Пусть T − это бинарное дерево, которое отличается от сортирующего лишь корневым элементом. Рассмотрим рекурсивную процедуру «пересыпки» Heap(first, last), которая преобразует дерево T в сортирующее дерево. Входные параметры first и last − это индексы первого и последнего передаваемого в процедуру элементов массива.

Алгоритм «пересыпки»

Из трёх элементов а[0], а[1] и а[2], соответствующих корню и двум его потомкам, выбрать элемент а[k] с максимальным ключом;

Если k 0, то поменять местами элементы а[0] и а[k];

Если k = 1, то рекурсивно применить процедуру Heap к левому поддереву, иначе (при k = 2) рекурсивно применить процедуру Heap к правому поддереву.

Древесная сортировка состоит из двух этапов. На первом этапе из элементов исходного массива строится сортирующее дерево. Этот этап осуществляется с помощью (n − 1)-го вызова процедуры Heap:

for (i=n-2; i>=0; i--) {Heap(i, n-1);}.

На втором этапе элемент с наибольшим ключом из корня сортирующего дерева переносится в свою, n−1-ю, позицию. Метка а[n−1] самой правой концевой вершины нижнего уровня переносится в корень, а сама концевая вершина удаляется. Полученное дерево с помощью процедуры Heap вновь перестраивается в сортирующее, и процесс повторяется:

for (i=n-1; i>0; i--) {поменять местами элементы а[0] и а[i]; Heap(0,i-1);}.

В ходе выполнения этого цикла элементы поочерёдно занимают своё место в отсортированном массиве.

60

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