Файл: Программная инженерия, 18. 04. 2013. Красноярск сфу 2013 2.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 25.10.2023
Просмотров: 52
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
при n = 1000 Метод Минимальное Среднее Максимальное Прямое включение С =
M =
999 1998 250250 247748 499499 498498 Прямой выбор С =
M =
499500 2997 499500 7478 499500 252997 Прямой обмен С =
M =
499500 0
499500 749250 499500 1498500
54 Для практических целей полезно иметь некоторые экспериментальные данные об эффективности того или иного алгоритма. В табл. 3.3 показано время работы (в секундах, обсуждавшихся выше методов сортировки, реализованных на персональной ЭВМ
Lilith. Три столбца содержат времена сортировки уже упорядоченного массива, массива со случайными числами и массива, расположенного в обратном порядке. Вначале приводятся цифры для массива, состоящего из 256 элементов, а затем – из 2048 элементов. Таблица 3.3 Время работы различных методов Метод Массив Упорядоченный Случайный В обратном порядке
n = 256 Прямое включение 0.02 0.82 1.64 Двоичное включение 0.12 0.70 1.30 Прямой выбор 0.94 0.96 1.18 Метод пузырька 1.26 2.04 2.80 Метод шейкера 0.02 1.66 2.92 Сортировка Шелла 0.10 0.24 0.28
n = 2048 Прямое включение 0.22 50.74 103.80 Двоичное включение 1.16 37.66 76.06 Прямой выбор 58.18 58.34 73.46 Метод пузырька 80.18 128.84 178.66 Метод шейкера 0.16 104.44 187.36 Сортировка Шелла 0.80 7.08 12.34 Очевидно преимущество сортировки Шелла как улучшенного метода, по сравнению с прямыми методами сортировки. Кроме того, можно отметить следующее
1. Улучшение двоичного включения по сравнению с прямым включением в действительности почти ничего не даст, а в случае упорядоченного массива даже получается отрицательный эффект.
2. Пузырьковая сортировка определенно наихудшая из всех сравниваемых. Ее усовершенствованная версия – шейкерная сортировка – продолжает оставаться плохой по сравнению с прямым включением и прямым выбором (за исключением патологического случая уже упорядоченного массива. Контрольные задания
1. Реализовать в программе два алгоритма из указанных ниже а) сортировка с помощью прямого включения, б) сортировка с помощью двоичного включения,
55 в) сортировка с помощью прямого выбора, г) шейкерная сортировка, д) сортировка Шелла. Сравнить эффективность реализованных алгоритмов по числу перестановок.
2. Запрограммировать три метода прямой сортировки. Сравнить в программе время выполнения каждого из методов.
3. Предположим, что необходимо отсортировать массив, состоящий из нескольких случайных элементов и следующих за ними упорядоченных элементов. Какой из рассмотренных в данной главе методов сортировки наиболее подходит для решения данной задачи
4. Алгоритм называется устойчивым, если он сохраняет исходный порядок элементов с одинаковыми значениями. Какой из алгоритмов, представленных в данной главе, устойчив
5. Рассмотрим алгоритм случайной сортировки, примененный к массиву целых чисел выбирается случайное число i из интервала от 1 дои меняются местами элементы a[0] и a[i], процесс повторяется до тех пор, пока не будут упорядочены все элементы массива. Каково ожидаемое время выполнения массива
6. Написать программу нахождения
k наименьших элементов в массиве длиной
n. Для каких значений k эффективнее сначала выполнить сортировку всего массива, а затем взять
k наименьших элементов, вместо поиска наименьших элементов в неупорядоченном массиве
56
4. СОРТИРОВКА ПОСЛЕДОВАТЕЛЬНОСТЕЙ Алгоритмы сортировки массивов, рассмотренные во третьей главе, предполагают, что объем данных позволяет разместить и произвести их сортировку, используя исключительно оперативную память компьютера. В том случае, если объем сортируемых данных значительно превышает возможности оперативной памяти, приходится задействовать устройства внешней памяти. Однако характеристики доступа к устройствам внешней памяти отличаются от характеристик доступа к оперативной памяти. Это вынуждает использовать отличные структуры и алгоритмы обработки данных. Рассматриваемые в данной главе алгоритмы предназначены для сортировки данных, представляющие собой (последовательный) файл, которые будем называть последовательность. Сортировка данных, организованных в виде файлов, называется внешней сортировкой. Для каждого файла характерно, что в каждый момент непосредственно доступна одна и только одна компонента. Это весьма сильное ограничение, если сравнивать с возможностями, предоставляемыми массивами, и поэтому приходится пользоваться другими методами сортировки.
4.1. Простое слияние Слияние означает объединение двух (или более) последовательностей в одну-единственную упорядоченную последовательность. Объединение происходит при помощи повторяющегося выбора элемента, удовлетворяющего заданному условию, из ряда доступных в данный момент элементов. Слияние намного проще сортировки, и его используют как вспомогательную операцию в более сложных процессах сортировки последовательностей. Наиболее простыми в тоже время основополагающим методом сортировки данных на основе слияния является сортировка простым слиянием. Она выполняется следующим образом
1. Последовательность а разбивается на две половины b и с. Разбиение происходит следующим образом первый элемент последовательности
a записывается в последовательность b, второй элемент последовательности записывается в последовательность c, третий – снова в b, четвертый – в
c и т. д.
2. Обе последовательности
b и с сливаются в a. При этом одиночные элементы последовательностей
b и с сравниваются и сливаются в последовательность в порядке возрастания (убывания, образуя упорядоченные пары.
57 3. Полученная последовательность а вновь разбивается на две – b и c. Данный шаг аналогичен шагу 1, однако разбиение происходит не на единичные элементы, а на упорядоченные пары. То есть первая пара последовательности записывается в последовательность b, вторая – в последовательность, третья пара последовательности a записывается в последовательность, четвертая – в c и т. д.
4. Слияние последовательностей
b и св последовательность a. При этом поочередно сравниваются элементы соответствующих пар последовательностей и си сливаются в последовательность a в порядке возрастания (убывания, образуя упорядоченные четверки.
5. Повторяя предыдущие шаги, разбиваем и сливаем четверки в восьмерки и т. д, каждый раз удваивая длину слитых подпоследовательностей до тех пор, пока не будет упорядочена целиком вся последовательность а. Рассмотрим в качестве примера следующую последовательность
a: 54 35 12 30 16 24 92 19 После разбиения (шаг 1) получаем последовательности
b: 54 12 16 92
c: 35 30 24 19 Слияние одиночных компонент, те. упорядоченных подпоследовательностей длины 1, на шаге 2 даст последовательность
a, состоящую из упорядоченных пар. Для удобочитаемости пары в последовательности ограничены апострофами
a: 35 54 ‘ 12 30 ‘ 16 24 ‘ 19 92 На шаге 3 последовательность
a будет разбита следующим образом
b: 35 54 ‘ 16 24
c: 12 30 ‘ 19 92 Последовательности
b и c также содержат упорядоченные пары. При слиянии этих последовательностей алгоритму доступен только один самый левый) элемент каждой последовательности. Сравнивание элементов последовательностей
b и c происходит только в соответствующих парах. Работа алгоритма на шаге 4 выглядит следующим образом
b: 35 54 ‘ 16 24
c: 12 30 ‘ 19 92 сравниваются элементы 12 итак как 12 < 35, то
a: 12
58 и
b: 35 54 ‘ 16 24
c: 30 ‘ 19 92 далее сравниваются элементы 30 итак как 30 < 35, то
a: 12 30 и
b: 35 54 ‘ 16 24
c: ‘ 19 92 Поскольку сравнивание происходит только в соответствующих парах, а пара последовательности с уже не содержит элементов, то пара последовательности полностью сливается в последовательность a:
a: 12 30 35 54 ‘ последовательности
b и c содержат элементы
b: 16 24
c: 19 92 Теперь начинается сравнение элементов вторых пар последовательностей и c и слияние их в последовательность а Результатом четвертого шага работы алгоритма будет последовательность а, содержащая упорядоченные четверки
a: 12 30 35 54 ‘ 16 19 24 92 Очередное разбиение последовательности а на последовательности b и сдаст Их слияние приводит, наконец, к желаемому результату
a: 12 16 19 24 30 35 54 92 Исходя из описания алгоритма сортировки и представленного выше примера, можно выделить два основных действия над последовательностями это разбиение исходной последовательности на две и их последующее слияние. Такие действия называются фазами фазы разбиения и фазы слияния (рис. 4.1). Наименьший подпроцесс, повторение которого составляет процесс сортировки, называется проходом или этапом. В приведенном примере сортировка происходит затри прохода, каждый из которых состоит из фазы разбиения и фазы слияния. Для выполнения данной сортировки потребовалось три файла или ленты, поэтому она называется трехленточным слиянием.
b
b
b
a
a
a
a
a
c
c
c фаза слияния фаза разбиения проход 1 проход 2 проход n Рис. 4.1. Фазы сортировки и проходы Фазы разбиения фактически не относятся к сортировке, ведь в них элементы не переставляются. В некотором смысле они непродуктивны, хотя и занимают половину всех операций по переписи. Если объединить разделение со слиянием, то от этих переписей можно вообще избавиться. Вместо слияния в одну последовательность результаты слияния будем сразу распределять по двум лентам, которые станут исходными для последующего прохода. В отличие от упомянутой двухфазной сортировки с помощью слияния будем называть такую сортировку однофазной. Она, очевидно, лучше, поскольку необходима только половина операций по переписи, но для этого приходится задействовать четвертую ленту. Анализ сортировки с помощью прямого слияния. Поскольку на каждом проходе длина подпоследовательностей увеличивается вдвое, и сортировка заканчивается, когда длина подпоследовательности станет больше или равной длине исходной последовательности, то всего потребуется) проходов. По определению на каждом проходе все n элементов копируются по одному разу, поэтому общее число перестановок
M = n * log(n). Число сравнений элементов С даже меньше M, так как при копировании остатков подпоследовательностей сравнения не производятся. Поскольку сортировка слиянием производится на внешних запоминающих устройствах, то затраты на операции пересылки на несколько порядков превышают затраты на сравнения. Поэтому детальный анализ числа сравнений особого практического интереса не представляет.
60
4.2. Естественное слияние При сортировке простым слиянием данные разбиваются и сливаются в подпоследовательности длина, которых равна степени двойки (2
k
, где
k ≥
0). Однако данные исходной последовательности могут быть уже частично упорядочены. В этом случае целесообразно просто объединить уже упорядоченные подпоследовательности. Две упорядоченные подпоследовательности длиной
m и n, содержащиеся в двух файлах, дадут на фазе слияния одну подпоследовательность из
m + n элементов. Сортировка, при которой сливаются упорядоченные подпоследовательности, называется естественным слиянием. Упорядоченные подпоследовательности называют сериями. В каждой серии элемент
r
i
не больше, чем
r
i+1
. Таким образом, в сортировке естественным слиянием объединяются серии, а непоследовательности заранее) фиксированной длины. Если сливаются две последовательности, каждая из
n серий, то результирующая также содержит ровно n серий. Следовательно, при каждом проходе общее число серий уменьшается вдвое и общее число пересылок в самом плохом случае равно
n * log(n), а в среднем даже меньше. Ожидаемое число сравнений, однако, значительно больше, поскольку кроме сравнений, необходимых для отбора элементов при слиянии, нужны еще дополнительные сравнения между последовательными элементами каждого файла, чтобы определить конец серии. Рассмотрим естественное слияние на примере двухфазной сортировки стремя лентами. Предположим, что файл
a содержит начальную последовательность элементов.
Файлы b и c – вспомогательные. Каждый проход состоит из фазы распределения серий из
a поочередно в b и c и фазы слияния, объединяющей серии из
b ив. Этот процесс соответствует показанному на рис. 4.1. Пусть исходная последовательность
a содержит следующие данные
a: 15 24 32 ‘ 18 45 ‘ 40 51 ‘ 21 29 31 43 ‘ 42 60 Серии в последовательности
a отделены апострофами. При разбиении исходной последовательности
a в последовательности b и c серии будут распределены следующим образом
b: 15 24 32 ‘ 40 51 ‘ 42 60
c: 18 45 ‘ 21 29 31 43 Как уже отмечалось, длина каждой серии не является заранее заданной, а вычисляется в процессе работы алгоритма. В данном примере в последовательность были записаны серии длиной 3, 2 и 2 элемента. Однако эти значения не запоминаются, а конец каждой серии определятся заново на очередной фазе. Последний элемент первой из записанных серий последовательности меньше первого элемента второй серии последовательности. Поэтому на фазе слияния эти две серии будут рассматриваться, как одна серия состоящая из пяти элементов
b: 15 24 32 40 51 ‘ 42 60 Таким образом, последовательность
a получается из слияния следующих последовательностей
b: 15 24 32 40 51 ‘ 42 60
c: 18 45 ‘ 21 29 31 43. Доступ к данным, хранящимся в файлах
b и c, аналогичен доступу в прямом слиянии. То есть в любой момент времени доступен один и только один элемент каждой последовательности. В демонстрируемом примере считываемые элементы находятся слева. Именно, эти элементы сравниваются, и наименьший из них записывается в файл
a. После естественного слияния последовательностей
b и c получаем
a: 15 18 24 32 40 45 51 ‘ 21 29 31 42 43 60. Наследующем шаге разбиение последовательности
a даст
b: 15 18 24 32 40 45 51
c: 21 29 31 42 43 60. Ив результате слияния этих двух последовательностей получаем отсортированную последовательность
a: 15 18 21 24 29 31 32 40 42 43 45 51 60. Таким образом, для сортировки исходной последовательности естественным слиянием понадобилось два прохода, в то время как для сортировки прямым слиянием понадобилось бы четыре прохода. Алгоритм сортировки естественным слиянием во многом похож на сортировку прямым слиянием. Единственное различие заключается с том, что длина подпоследовательностей в прямом слиянии кратна двойке 1, 2,
4, 8, 16 и т. дав естественном слиянии длина серии вычисляется в процессе считывания элементов последовательности. И именно этим реализация алгоритма естественного слияния оказывается сложнее. Серьезную трудность представляет собой определение конца серии, поскольку для этого необходимо сравнивать значения двух последовательных элементов. Однако природа последовательности такова, что непосредственно доступен только один-единственный элемент. Для того чтобы заглянуть вперед можно организовать своеобразный буфер, введя переменную, куда будет считываться текущий (самый левый) элемент последовательности. Таким образом, можно будет сравнить только что считанный элемент, который теперь хранится в буфере, со следующим за ним элементом последовательности. Если элемент, считанный в буфер, меньше следующего за ним, то элементы принадлежат одной серии. Если же элемент, содержащийся в буфере, оказывается больше последующего элемента, это обозначает конец текущей серии и начало новой. Программный код разбиения исходной последовательности
a на серии и запись их в последовательности
b и c представлен ниже. Имя пере- менной-буфера, в которой сохраняется значение прочитанного элемента последовательности, –
buf. Для файла с исходной последовательностью a используется указатель
f, для последовательностей b и c – указатели file[0] и
file[1], соответственно. Все три указателя имеют тип FILE *.
f = исходный файл a>, "r");
file[0] = файл b>, "w");
file[1] = файл c>, "w");
fscanf(f, "%d", &elem);
// считали один элемент из
// последовательности а
fprintf( file[0], "%d ", elem);
// записали один элемент в
b
buf = elem; // buf – переменная-буфер, содержащая значение
// текущего элемента последовательности
a
j = 0;
//
j служит индексом для записи в последовательности b и
c,
// если
j = 0 запись происходит в последовательность b,
// если
j = 1 запись происходит в последовательность с
while( !feof(f) )
// перепись одного элемента в файл
b или c
{
fscanf(f, "%d", &elem);
if( elem >= buf )
// серия не кончилась, запись в тот же файл
{
fprintf( file[j], "%d ", elem);
buf = elem;
}
else
// серия закончилась, начало записи в другой файл
{
j = 1 – j; // индекс
j теперьуказывает на другой файл
fprintf( file[j], "%d ", elem);
buf = elem;
}
}
63 Если ввести переменную, в которой при слиянии будут подсчитываться серии, то работу алгоритма можно завершить, когда в последовательности будет всего одна серия. Это свидетельствует о том, что исходная последовательность уже отсортирована.
4.3. Многопутевая сортировка Затраты на сортировку последовательностей пропорциональны числу выполняемых проходов, так как при каждом проходе копируются все данные. Одним из подходов к повышению эффективности сортировок слиянием является вовлечение в процесс дополнительных каналов обмена данными. Распределение серий на более чем две ленты позволит значительно сократить количество перестановок элементов. Сортировка, при которой используется несколько файлов, называется многопутевым или путевым слиянием. Общее число проходов необходимых для сортировки многопутевым слиянием последовательности из
n элементов равно log
N
(
n). Поскольку при каждом проходе выполняется n операций копирования, тов худшем случае общее число перестановок будет равным
M = n * log
N
(
n). При сортировке путевым слиянием используются 2 * N последовательностей. Предполагается, что число входных файлов равно числу выходных. На каждом проходе серии входных последовательностей сливаются, объединяясь, в выходные последовательности. После того, как данные во входных последовательностях заканчиваются, последовательности меняются ролями – входные последовательности становятся выходными и наоборот.
Многопутевое слияние основывается на естественном слиянии с од- ной-единственной фазой. Также как ив случае обычного двухфазного трехленточного естественного слияния возможно слияние двух следующих одна за другой серий в одну, если последний элемент какой-либо серии меньше первого элемента серии, следующей за ней. Рассмотрим в качестве примера сортировку путевым слиянием следующей последовательности
f: 57 24 88 13 19 17 96 37 42 15 21 35 23 10 53 49 33 58 16 72. Для путевого слияния будут использованы 6 файлов 3 входных и 3 выходных. Первым этапом сортировки многопутевым слиянием является разбиение исходной последовательности. Все данные разбиваются на серии и записываются в файлы
f
1
,
f
2
,
f
3
. Серии в этих файлах отделены апострофами ‘ 49
f
3
: 13 19 ‘ 15 21 35 ‘ 33 58 Поскольку в основе многопутевой сортировки лежит естественное слияние, то длина каждой серии заранее неизвестна и определяется непосредственно в процессе чтения данных из файла. Указанные выше файлы
f
1
,
f
2
,
f
3
послужат на первом проходе входными файлами, те. из них будут считываться серии и сливаться в выходные файлы
f
4
,
f
5
,
f
6
. Вначале работы алгоритма сортировки выходные файлы пусты. На первой итерации первого прохода первые серии входных файлов сливаются в одну серию, записываемую в выходной файл
f
4
:
f
1
: 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57 88
f
2
: 37 42 ‘ 10 53 ‘ 49
f
5
: .
f
3
: 15 21 35 ‘ 33 58
f
6
: Результатом второй итерации первого прохода будут слияние вторых серий входных файлов в выходной файл
f
5
:
f
1
: 23 ‘ 16 72
f
4
: 13 19 24 57 88
f
2
: 10 53 ‘ 49
f
5
: 15 17 21 35 37 42 96.
f
3
: 33 58
f
6
: На третей итерации первого прохода будет заполнен выходной файл
f
6
:
f
1
: 16 72
f
4
: 13 19 24 57 88
f
2
: 49
f
5
: 15 17 21 35 37 42 96.
f
3
:
f
6
: 10 23 33 53 58 Первый проход завершается на четвертой итерации. Входной файл
f
3 пуст, поэтому в выходной файл
f
4
сливаются серии, находящиеся только в файлах
f
1
и f
2
, все данные входных файлов заканчиваются
f
1
:
f
4
: 13 19 24 57 88 ‘ 16 49 72.
f
2
:
f
5
: 15 17 21 35 37 42 96
f
3
:
f
6
: 10 23 33 53 58 Если бы во входных файлах еще содержались данные, то серии сливались бы в файл
f
5
, затем f
6
, снова
f
4
,
f
5
,
f
6
и т. д, пока не закончатся все данные во всех входных файлах. После того, как входные файлы становятся пустыми, выполняется второй проход. На втором проходе входные файлы
f
1
,
f
2
,
f
3
становятся выходными и уже в них сливаются серии из файлов
f
4
,
f
5
,
f
6
, которые на втором проходе становятся входными
f
4
: 13 19 24 57 88 ‘ 16 49 72
f
1
:
f
5
: 15 17 21 35 37 42 96
f
2
:
f
6
: 10 23 33 53 58
f
3
: Результат первого слияния на втором проходе алгоритма следующий 58 88 96
f
5
:
f
2
:
f
6
:
f
3
: Второй проход заканчивается на втором слиянии из единственного содержащего данные файла
f
4 в выходной файл
f
2
f
4
:
f
1
: 10 13 15 17 19 21 23 24 33 35 37 42 53 57 58 88 96
f
5
:
f
2
: 16 49 72
f
6
:
f
3
: На третьем проходе входные и выходные файлы опять меняются ролями, теперь
f
1
,
f
2
,
f
3
– входные файлы, а
f
4
,
f
5
,
f
6
– выходные. Алгоритм путевого слияния заканчивает свою работу на третьем проходе. На этом проходе выполняется только одна итерация, на которой серии из файлов
f
1
и f
2
сливаются в файл
f
4
:
f
1
:
f
4
: 10 13 15 16 17 19 21 23 24 33 35 37 42 49 53 57 58 72 88 96
f
2
:
f
5
:
f
3
:
f
6
: Полученную последовательность можно для удобства записать вис- ходный файл
f. Рассмотренный пример показывает, что алгоритм многопутевого слияния работает эффективнее, чем рассмотренные ранее алгоритмы прямого и естественного слияния. Так, при путевом слиянии для сортировки указанной последовательности потребовалось только три прохода, в то время как для естественного и прямого слияния потребовалось бы четыре и пять проходов, соответственно. К тому же последние два алгоритма требуют двойного копирования, поскольку состоят из двух фаз разбиения и слияния.
В нашем примере используются шесть входных и выходных файлов, но для более эффективной работы их число может быть увеличено. Это требуется для работы с большими последовательностями, содержащими большое количество элементов. В этом случае будет затрачено значительно меньше времени на распределение и последующее слияние. Теперь рассмотрим несколько вопросов связанных с реализацией алгоритма многопутевого слияния. Поскольку при сортировке последовательностей многопутевым слиянием используется достаточно большое количество файлов, то для ссылки на файлы целесообразно использовать не отдельные переменные, а массив файлов. Если объявить имена и указатели на используемые файлы следующим образом
FILE *file[6];
char filename[6][10] = {{"f1.txt"},
66
{"
f2.txt"},
{"
f3.txt"},
{"
f4.txt"},
{"
f5.txt"},
{"
f6.txt"}}; то можно легко открыть на чтение и запись соответствующие файлы
for(i = 0; i < 3; i++)
// первые три файла открываются на чтение
file[i] = fopen(filename[i],"r");
for(i = 3; i < 6; i++) // остальные три файла открываются на запись
file[i] = fopen(filename[i],"w"); Такой подход обладаем еще одним серьезным преимуществом. Теперь для доступа к какому-либо файлу можно использовать его индекс, что крайне полезно при переключении группы входных и выходных последовательностей в конце каждого прохода. Для решения задачи переключения лент можно рекомендовать технику карты индексов лент. Вместо прямой адресации ленты с помощью индекса
i она адресуется через карту t, которая объявляется как
int t[2 * N]; // 2 * N – общее число входных и выходных файлов. Вначале работы алгоритма
t
i
=
i для всех i. Переключение же входных и выходных файлов представляет собой обмен местами компонент
t
i
и
t
i + N
для всех
i = 1, …, N. В этом случае всегда можем считать, что f
t1
, …,
f
tN
– входные последовательности, a
f
t(N + 1)
, …,
f
t(2*N)
– выходные. Для описанного выше примера путевой (
N = 3) сортировки карта индексов лент определяется как
int t[6];
for(i = 0; i < 2 * N; i++)
t[i] = i; Начальные значения элементов данной карты индексов лент представлены в табл. 4.1. Здесь также указаны файлы, на которые ссылается каждый элемент карты индексов лент. Таблица 4.1 Карта индексов лент
t
i
i Ссылка на файл
t[1] 1
f
1
t[2] 2
f
2
t[3] 3
f
3
t[4] 4
f
4
t[5] 5
f
5
t[6] 6
f
6
67 При переключении входных и выходных файлов меняются местами компоненты
t
i
и
t
i + 3
, для всех
i = 1, …, 3. Результат этого процесса показан в табл. 4.2. Таблица 4.2 Карта индексов лент после переключения входных и выходных файлов
t
i
i Ссылка на файл
t[1] 4
f
4
t[2] 5
f
5
t[3] 6
f
6
t[4] 1
f
1
t[5] 2
f
2
t[6] 3 Таким образом, входные последовательности всегда находятся вверх- ней половине карты индексов лент и к ним всегда можно получить доступ через
t[1], t[2] и t[3]. Выходные последовательности находятся в нижней половине, доступ к ним всегда осуществляется через
t[4], t[5] и t[6]. Открыть входные файлы на чтение, а выходные на запись при помощи карты индексов лент можно следующим образом
for(i = 0; i < 3; i++) // открыть входные файлы на чтение
file[t[i]] = fopen (filename[t[i]],"r");
for(i = 3; i < 6; i++) // открыть выходные файлы на запись
file[t[i]] = fopen (filename[t[i]],"w"); Закрыть файлы при помощи карты индексов лент можно так
for(i = 0; i < 6; i++) // закрыть все файлы
fclose(file[t[i]]); Преимущество использования карты индексов лент заключается том, что приведенный код остается неизменным независимо оттого, являются входными файлами
f
1
,
f
2
,
f
3
или же
f
4
,
f
5
,
f
6
. Главной задачей становится указание, на какой файл должен ссылаться каждый элемент карты индекса лент При новом проходе входные файлы становятся выходными, те. их индексы перемещаются во вторую половину карты индексов лент, выходные же файлы становятся входными – их индексы перемещаются в первую половину карты индексов лент. Таким образом происходит переиндекса- ция файлов. Затем входные файлы открываются на чтение, выходные – на запись, и выполняется поочередное слияние серий из входных в файлов в выходные. В завершении каждого прохода все файлы закрываются.
68 При слиянии серий необходимо точно идентифицировать текущие входные последовательности. Может оказаться, что число входных файлов будет меньше, чем
N. Данная ситуация характерна для последних проходов. Так, в примере путевого слияния вначале третьего прохода число входных файлов равно двум. Поэтому следует ввести некоторую переменную, скажем
k1, задающую фактическое число работающих входов. Инициализация может выглядеть следующим образом
if (L < N) k1 = L;
else k1 = N; где
L – число серий, полученных при последнем слиянии. Возвращаясь к примеру путевого слияния, можно сказать, что вначале первого прохода
k1 = 3, в то время, как вначале второго слияния на втором проходе, где входной файл
f
4
содержит элементы 16, 49, 72, а остальные два входных файла
f
5
и f
6
пусты,
k1 = 1. Понятно, что по исчерпании какого-либо входа
kl должен уменьшаться. Слияние серий входных файлов включает в себя повторяющийся выбор наименьшего из элементов соответствующих серий и отсылку его в выходной файл. Этот процесс усложняется необходимостью определять конец каждой серии. Конец серии достигается в двух случаях очередной элемент меньше текущего элемента достигнут конец входного файла. В последнем случае вход исключается из работы и
kl уменьшается. В первом же серия закрывается, элементы соответствующей последовательности в выборе уже не участвуют, но это продолжается лишь до того, как будут считаны все данные соответствующих серий из других входных последовательностей. Отсюда следует, что нужна еще одна переменная, скажем
k2, указывающая число входов, действительно используемых при выборе очередного элемента. Вначале ее значение устанавливается равными уменьшается всякий раз, когда серия заканчивается. Опять же возвращаясь к ранее рассмотренному примеру, вначале первого прохода имеем
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: 13 19 ‘ 15 21 35 ‘ 33 58 Входными файлами являются
f
1
,
f
2
,
f
3
, переменные
k1 и k2 равны трем. При слиянии первых серий этих трех последовательностей в выходной файл
f
4
значение
k2 изменяется следующим образом
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: 19 ‘ 15 21 35 ‘ 33 58
k2 = 3
69
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2 (закончилась серия в
f
3
)
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 1 (закончилась серия в
f
1
)
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57 88
f
2
: ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 0 (закончилась серия в
f
2
) Переменная
k2 приняла значение ноль, значит, данные первых серий закончились. Переходим к слиянию вторых серий входных последовательностей. Выполняется проверка на наличие пустых входных файлов. Поскольку все три входных файла содержат данные, то
k1 = 3, и переменная, отражающая число активныхвходов, инициализируется значением
k1, те. Начинается слияние вторых серий в выходной файл Можно сказать, что переменная
k1 носит более глобальный характер, поскольку показывает, сколько всего входных файлов находятся в работе. Переменная
k2 отвечает за количество активных входных файлов, те. таких файлов, чьи серии участвуют в слиянии в данный момент. Значение переменной
k2 не может превышать значения k1. К сожалению, недостаточно просто ввести переменную
k2. Необходимо знать не только число активных последовательностей, но и какие, именно, из них используются. Очевидное решение – использовать массив булевых переменных, определяющих доступность последовательностей. Однако существует и другой метод, при котором процедура выбора становится более эффективной, а это, в конечном счете, наиболее часто повторяющаяся часть алгоритма. Вместо булевого массива вводим вторую карту лент
ta, где ta
1
, …,
ta
k2
– индексы доступных входов.
Эта карта будет использоваться вместо карты
t. Размер карты ta в два раза меньше размера карты t, так как она используется для индексации только входных файлов. Вначале каждого прохода карта индексов активных лент
ta инициализируется следующим образом
for(i = 0; i < k1; i++)
ta[i] = t[i];
70 Слияние серий из входных файлов нас помощью карты индексов активных лент
ta можно записать следующим образом
k2 = k1;
do
{ выбор минимального элемента,
ta[mx] – индекс файла, в котором находится минимальный элемент перемещение минимального элемента изв если файл
file[ta[mx]] пуст – исключить входной файл если в файле
file[ta[mx]] закончилась серия – закрыть серию
}
while(k2 != 0); Операция исключить файл»
предполагает уменьшение kl и k2, а также переупорядочение индексов в карте
ta. Оператор закрыть серию»
уменьшает только
k2 и переупорядочивает соответствующим образом ta. Рассмотрим переупорядочение индексов в карте
ta на приведенном ранее примере путевого слияния. На первом проходе входными файлами являются
f
1
,
f
2
,
f
3
, первое слияние серий происходит в файл
f
4
. Начальное распределение серий по входным файлам выглядит следующим образом
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49.
f
3
: 13 19 ‘ 15 21 35 ‘ 33 58 Помимо значений элементов, содержащихся во входных и выходных файлах, приведем значение переменной
k2 и карты индексов лент ta.
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13
ta
1
= 1
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 2
f
3
: 19 ‘ 15 21 35 ‘ 33 58
k2 = 3
ta
3
= Видно, что первый индекс карты
ta указывает на первый файл, второй индекс – на второй файл, третий индекс – на третий. Так как
k2 = 3, то при слиянии используются все три индекса карты
ta, а, следовательно, и все три входных файла, на которые указывают эти индексы.
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19
ta
1
= 1
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 2
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2
ta
3
= 3 (не исп) Серия в файле
f
3
закончилась, поэтому значение переменной
k2 уменьшается на единицу. Это, в свою очередь, означает, что будут использоваться только первые два индекса карты
ta: ta
1
и
ta
2
. Последний, третий, индекс
ta
3
не будет использоваться до тех пор, пока не начнется слияние вторых серий входных файлов. Индекс
ta
3
, который больше не принимается во внимание, должен указывать на тот файл, в котором только что закончилась серия. В данном случае это файл
f
3
, поэтому
ta
3 должен иметь значение 3. (Конечно, они раньше был равен трем, но важным здесь является то, что индекс
ta
3
, который больше не рассматривается, указывает на третий входной файл. Очередное слияние серий приведет к следующему расположению элементов в файлах
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24
ta
1
= 1
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 2
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2
ta
3
= 3 (не исп) Значение
k2 не изменилось, так как серии в файлах f
1
и
f
2
не закончились. Поскольку при слиянии использовались только индексы
ta
1
и
ta
2
, то файл
f
3
, на который указывает индекс в слиянии не участвовал. При очередном слиянии получаем
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57
ta
1
= 2
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 1 (не исп)
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 1
ta
3
= 3 (не исп) Теперь закончилась серия в файле
f
1
. Значение переменной
k2 уменьшается на единицу. Это значит, что индекс
ta
2
больше не рассматривается. Данный индекс должен получить значение 1, те. указывать на файл с закончившейся серией
f
1
(
ta
2
= 1). Однако нельзя терять его бывшее значение – 2. Это значение должно быть присвоено тому индексу, который ранее указывал на файл
f
1
. Этим индексом является
ta
1 и поэтому
ta
1
= 2. То есть теперь индекс
ta
1 указывает на файл
f
2
, единственный файл, в котором еще осталась незаконченная серия. Следующее слияние приведет к тому, что все элементы первых серий входных файлов закончатся
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 2 (не исп)
f
2
: ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 1 (не исп)
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 0
ta
3
= 3 (не исп) Значение переменной
k2 станет равным нулю, те. индексы карты ta больше не используются. Так заканчивается слияние первых серий входных последовательностей. Можно лишь добавить, что значение переменной все это время оставалось равным трем, так как не был достигнут конец ни одного из трех входных файлов. Наследующем цикле выполняется присвоение
k2 = k1, те. Это значит, что все индексы карты ta снова используются при слиянии. Значения
ta
i
при этом сохраняются, те. индекс
ta
1 указывает на файл
f
2
,
ta
2 указывает на
f
1
, а
ta
3 на файл
f
3
. Порядок входных файлов в карте индексов не имеет значения. Существенным является лишь то, что все индексы карты индексов активных лент
ta указывают на все существующие входные последовательности. Как уже отмечалось, значение переменной
k1 уменьшается на единицу при достижении конца одного из файлов. Увидеть это можно на
72 третьей итерацией первого прохода, где слияние происходит в выходной файл
f
6
f
1
: 23 ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 1
f
2
: 10 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 33 58
k1 = 3
k2 = 3 f
6
:
ta
3
= 3
f
1
: 23 ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 1
f
2
: 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 33 58
k1 = 3
k2 = 3
f
6
: 10
ta
3
= 3
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3
f
2
: 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 33 58
k1 = 3
k2 = 2
f
6
: 10 23
ta
3
= 1 (не исп)
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3
f
2
: 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 58
k1 = 3
k2 = 2
f
6
: 10 23 33
ta
3
= 1 (не исп)
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3
f
2
: ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2 (не исп)
f
3
: 58
k1 = 3
k2 = 1
f
6
: 10 23 33 53
ta
3
= 1 (не исп)
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3 (не исп)
f
2
: ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2 (не исп)
f
3
:
k1 = 2
k2 = 0
f
6
: 10 23 33 53 58
ta
3
= 1 (не исп) Наследующей итерации произойдет слияние входных файлов
f
1
ив файл
f
4
, и первый проход завершится. При новом проходе входные ивы- ходные файлы должны поменяться ролями
for(i = 0; i < N; i++)
{
temp = t[i]; t[i] = t[i + N]; t[i + N] = temp; } Проходы выполняются до тех пор, пока не останется последовательность, состоящая из одной-единственной серии. Резюмируя все вышесказанное, можно дать общий набросок программы, реализующий алгоритм многопутевого слияния инициализация индексов карты лент
t
i
, для всех
i = 1, …, 2 * N; распределение серий исходной последовательности во входные файлы
t[1],
…,
t[N], при этом подсчитывается общее количество серий
L; пока
L > 1
{ если
L < N, тогда k1 = L, иначе k1 = N;
73 инициализация индексов карты активных входов
ta
i
=
t
i
, для
i = 1, …, N;
j = N + 1, где j – индекс текущего выходного файла пока
k1 > 0
{
L = L +1;
k2 = k1; пока
k2 > 0
{ поиск минимального элемента в рассматриваемых сериях, ta[mx] – индекс файла, в котором содержится мин. элемент запись минимального элемента изв если конец файла
ta[mx], то k1 = k1 – 1, k2 = k2 – 1, переупорядочивание индексов карты
ta; иначе если серия в
ta[mx] закончилась, то k2 = k2 – 1, переупорядочивание индексов карты
ta;
} смена выходного файла если
j < 2 * N, тогда j = j + 1, иначе j =
N + 1;
} входные и выходные файлы меняются ролями
t
i
меняется с
t
i + N
, для
i = 1, …, N;
}
4.4. Многофазная сортировка В основе сортировки последовательности многофазным слиянием лежит распределение начальных серий в соответствии с числами Фибоначчи. Поэтому, перед тем, как перейти непосредственно к алгоритму многофазной сортировки, рассмотрим числа Фибоначчи. Числа Фибоначчи В 1202 году итальянский математик Леонардо Фибоначчи поставил в своей книге «Liber abacci» следующую задачу Человек посадил пару кроликов в загон, окруженный со всех сторон стеной. Сколько пар кроликов будет через
n месяцев, если известно, что через месяц каждая пара кроликов производит на свет еще одну пару, которая в свою очередь становится способной производить потомство со второго месяца после своего рождения В этой же книге Фибоначчи приводит решение этой задачи количество пар кроликов будет увеличиваться каждый месяц следующим образом
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
74 Иными словами, число пар кроликов создает ряд, каждый член которого равен сумме двух предыдущих. Этот ряд известен как ряд Фибоначчи, асами числа – как числа Фибоначчи.
f
1
f
2
f
3 13(1) 8(1)
5(1) 0 8(2)
0 5(3) 3(2)
3(5) 2(3) 0 1(5) 0 2(8)
0 1(13) 1(8)
1(21) 0 0 Рис. 4.2. Многофазная сортировка стремя последовательностями На рис. 4.2 показано распределение серий при многофазной сортировке слиянием стремя последовательностями, две из которых являются входными, одна – выходной. На каждом проходе элементы из двух входных последовательностей сливаются в третью. Как только одна из входных последовательностей исчерпывается, она становится выходной. Числа, указанные на рис. 4.2, показывают количество серий в последовательности и длину серий. Слияние начинается с двух последовательностей
f
1
и
f
2
, организованных в виде серий длины 1. Серии из
f
1
и
f
2 объединяются, образуя серии длины 2, и записываются в третью последовательность
f
3
. Слияние происходит до полного опустошения последовательности
f
2
(как видно из рис.
4.2 в последовательности
f
2
серий меньше, чем в
f
1
). Затем объединяем оставшиеся серии длины 1 из
f
1
с таким же количеством серий длины 2 из В результате получаются серии длины 3, которые помещаются в файл Затем объединяются серии длины 2 из
f
3
с сериями длины 3 из
f
2
. Эти серии длины 5 помещаются в последовательность
f
1
, которая была исчерпана вовремя предыдущего прохода. Процесс продолжается до тех пор, пока вне окажется отсортированная последовательность. Оказывается, чтобы сохранить нужный ход процесса сортировки, начальное количество серий в
f
1
и
f
2
должно представлять собой два последовательных числа Фибоначчи. Так на рис. 4.2 показан процесс сортировки серии, распределенных следующим образом 13 серий в последовательности и 8 серий – в В табл. 4.3 приведено количество серий во входных файлах
a
1
(L)
и
a
2
(L) и необходимое для их слияния число проходов
L. Понятие прохода при многофазной сортировке менее жесткое, чем в рассмотренных ранее алгоритмах внешней сортировки, и представляет собой слияние серий из входных файлов в выходной, пока один из входных файлов не станет пустым. Как только опустошается один из входных файлов, проход считается завершенным, несмотря на то, что в других входных файлах еще остались серии, не участвовавшие в слиянии. Опустошившийся входной файл на новом проходе становится выходным, все остальные файлы – входными. Таблица 4.3 Идеальное распределение серий по двум последовательностям
L
a
1
(L)
a
2
(L)
Sum a
i
(L)
0 1 0 1 1 1 1 2 2 2 1 3 3 3 2 5 4 5 3 8 5 8 5 13 6 13 8 21 В табл. 4.3 также приведена сумма серий в обеих входных последовательностях. Если забыть об условии, что количество серий, хранящихся в последовательных файлах, заранее неизвестно, то можно сказать, что идеальным для многофазной сортировки будет количество серий равное одному из чисел, приведенных в четвертом столбце табл. 4.3. Здесь же приводится и идеальное распределение серий по входным последовательностям. Так, в рассмотренном примере, исходная последовательность содержит серию, что является идеальным числом серий для алгоритма многофазного слияния. Во входные последовательности будут распределены соответственно 13 и 8 серий. Как видно из табл. 4.3, для сортировки данной последовательности потребуется шесть проходов. Если бы исходная последовательность содержала 8 серий, то 5 из них были бы распределены в первую входную последовательность, 3 – во вторую. Исходная последовательность была бы отсортирована за четыре прохода. Из табл. 4.3 при
L > 0 можно вывести следующие соотношения
a
2
(L + 1)
=
a
1
(L)
,
a
1
(L + 1)
=
a
1
(L)
+
a
2
(L)
,
76 и
a
1
(0)
= 1,
a
2
(0)
= 0. Принимая
f
i
+ 1
=
a
1
(i)
, получаем для
i > 0:
f
i + 1
=
f
i
+
f
i – 1
,
f
1
= 1,
f
0
= 0. Эти рекуррентные отношения задают числа Фибоначчи
f = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... Эти числа Фибоначчи называются числами Фибоначчи первого порядка. Существуют числа Фибоначчи и более высоких порядков. В общем виде числа Фибоначчи порядка
p определяются следующим образом
f
(p)
i + 1
=
f
(p)
i
+ f
(p)
i – 1
+ … + f
(p)
i – p
для
i ≥ p,
f
(p)
p
= 1,
f
(p)
i
= 0 для 0 ≤
i < p. Рассмотрим второй пример, где в многофазном слиянии участвуют шесть последовательностей, одна из которых – выходная. Данный пример показан на рис. 4.3. В данном примере начальное распределение серий следующее в находится 16 серий, в
f
2
– 15, в
f
3
– 14, в
f
4
– 12 серий и 8 серий в
f
5
. Напер- вом проходе сливаются и помещаются в
f
6 8 серий. Обратите внимание, что длина этих восьми серий равна пяти, поскольку в слиянии участвовали серии длины 1, хранящиеся в пяти входных файлах. Процесс слияния серий аналогичен слиянию стремя последовательностями. Заканчивается многофазное слияние с шестью последовательностями через пять проходов, когда
f
2
содержит все отсортированное множество элементов.
f
1
f
2
f
3
f
4
f
5
f
6 16(1) 15(1) 14(1) 12(1) 8(1)
8(1)
7(1)
6(1)
4(1)
0 8(5)
4(1)
3(1)
2(1)
0 4(9) 4(5)
2(1)
1(1)
0 2(17) 2(9) 2(5)
1(1)
0 1(33)
1(17) 1(9) 1(5)
0 1(65)
0 0
0 0 Рис. 4.3. Многофазная сортировка с шестью последовательностями В примере многофазного слияния с шестью последовательностями выполнялась сортировка 65 элементов (или серий длины 1). Такое количество было выбрано преднамеренно, поскольку идеально подходит для многофазной сортировки с шестью последовательностями, в которой используются числа Фибоначчи четвертого порядка. То есть порядок используемых чисел Фибоначчи зависит от количества последовательностей при многофазной сортировки с
N последовательностями используются числа Фибоначчи
N – 2 порядка. В табл. 4.4 приведено распределение серий по пяти входным последовательностям, соответствующих приведенному на рис. 4.3 примеру многофазной сортировки с шестью последовательностями. Таблица 4.4 Идеальное распределение серий по пяти последовательностям
L
a
1
(L)
a
2
(L)
a
3
(L)
a
4
(L)
a
5
(L)
Sum
a
i
(L)
0 1 0 0 0 0 1
1 1 1 1 1 1 5
2 2 2 2 2 1 9
3 4 4 4 3 2 17 4 8 8 7 6 4 33 5 16 15 14 12 8 65 Исходя из табл. 4.4, можно вывести правила образования числа серий на каждом проходе
a
5
(L + 1)
=
a
1
(L)
,
a
4
(L + 1)
=
a
1
(L)
+
a
5
(L)
=
a
1
(L)
+
a
1
(L – 1)
,
a
3
(L + 1)
=
a
1
(L)
+
a
4
(L)
=
a
1
(L)
+
a
1
(L – 1)
+
a
1
(L – 2)
,
a
2
(L + 1)
=
a
1
(L)
+
a
3
(L)
=
a
1
(L)
+
a
1
(L – 1)
+
a
1
(L – 2)
+
a
1
(L – 3)
,
a
1
(L + 1)
=
a
1
(L)
+
a
2
(L)
=
a
1
(L)
+
a
1
(L – 1)
+
a
1
(L – 2)
+
a
1
(L – 3)
+
a
1
(L – 4)
, Подставляя
f
i
вместо
a
1
(i)
, получаем
f
i + 1
=
f
i
+
f
i – 1
+
f
i – 2
+
f
i – 3
+
f
i – 4
, для
i ≥ 4,
f
4
= 1,
f
i
= 0, для
i < 4, что полностью соответствует приведенным выше правилам, определяющим числа Фибоначчи порядка
p. Таким образом, любое из чисел Фибоначчи четвертого порядка равно сумме пяти предшествующих ему чисел. Ряд Фибоначчи четвертого порядка выглядит следующим образом
f
(4)
= 0, 0, 0, 0, 1, 1, 2, 4, 8, 16, ... Эти числа можно увидеть в табл. 4.4 в столбце, соответствующему
a
1
(L)
78 В завершение обсуждения чисел Фибоначчи приведем табл. 4.5, в которой содержится количество серий, идеально подходящих для многофазной сортировки с
N последовательностями, значение L соответствует числу проходов, необходимых для сортировки всех элементов исходной последовательности. Таблица 4.5 Количество серий, допускающее идеальное распределение
L N
3 4 5 6 7 8 1 2 3 4 5 6 7 2 3 5 7 9 11 13 3 5 9 13 17 21 25 4 8 17 25 33 41 49 5 13 31 49 65 81 97 6 21 57 94 129 161 193 7 34 105 181 253 321 385 8 55 193 349 497 636 769 9 89 355 673 977 1261 1531 10 144 653 1297 1921 2501 3049 11 233 1201 2500 3777 4961 6073 12 377 2209 4819 7425 9841 12097 13 610 4063 9289 14597 19521 24097 14 987 7473 17905 28697 38721 48001 Элементами табл. 4.5 являются суммы серий
n последовательностей на определенном проходе. Так значения
Sum a
i
(L)
из табл. 4.3 можно найти в табл. 4.5 с индексом столбца
N = 3 (в многофазной сортировке используются три последовательности. Значения
Sum a
i
(L)
из табл. 4.4 находятся в табл. 4.5 с индексом столбца
N = 6 (используются шесть последовательностей. Начальное распределение серий для алгоритма многофазной сортировки В рассмотренных примерах слияния предполагалось, что количество серий в исходной последовательности имеет значение, соответствующее одному из чисел Фибоначчи, что позволит произвести начальное распределение серий идеальным образом. На практике же это условие выполняется редко. В этом случае предлагается ввести гипотетические пустые серии, которые в сумме с реально существующими сериями дадут число, подходящее для идеального распределения серий. Новые введенные серии называются пустыми сериями. Пустые серии необходимо распределять по входным последовательностям как можно более равномерно, поскольку эффективность алгоритма
79 многофазной сортировки зависит от слияния реальных серий. Если серия последовательности пустая, тов слиянии она не участвует, и слияние происходит не из
N – 1 входных последовательностей, а из меньшего их числа. А при многофазной сортировке должно использоваться как можно большее число последовательностей. Если во всех
N – 1 последовательностях содержатся пустые серии, то никакого реального слияния не происходит, а выходную последовательность записывается пустая серия. Рассмотрим задачу распределения серий исходной последовательности по
N – 1 последовательностям. Поскольку мы имеем дело с внешней сортировкой, то число серий заранее неизвестно. В процессе распределения вычисляются числа Фибоначчи порядка
N – 2, определяющие желательное число серий в каждой из входных последовательностей. Предположим, что общее число последовательностей
N = 6, из которых при сортировке пять будут входными. Следовательно, необходимо распределить серии по этим пяти последовательностям. Так как при распределении серии записываются в пять последовательностей, воспользуемся табл. 4.4. Сначала серии считываются из исходной последовательности и распределяются, как указано в строке с индексом, если в исходной последовательности еще остались серии, то переходим ко второй строке (2, 2, 2, 2, 1), если серии все еще остались, то переходим к третьей строке (4, 4, 4, 3, 2) и т. д. Будем называть индекс строки уровнем Очевидно, чем больше число серий, тем выше уровень чисел Фибоначчи, который в данном случае равен количеству проходов, необходимых для сортировки исходной последовательности. Проиллюстрируем распределение серий на примере с пятью последовательностями. Для оптимального распределения серий воспользуемся алгоритмом горизонтального распределения. Сначала записываем в последовательности по одной серии (рис. 4.4). Числа на рис. 4.4 означают порядок серий, те. первая серия исходной последовательности записывается в
f
1
, вторая серия – в
f
2
и т. д.
1 2 3 4 5 Рис. 4.4. Горизонтальное распределение серий, L = 1 Если исходная последовательность еще содержит серии, то переходим на второй уровень, где число серий в последовательностях
f
1
,
f
2
,
f
3
,
f
4
,
f
5
. должно быть равно, соответственно, 2, 2, 2, 2, 1. На рис. 4.5 показаны серии последовательностей, записанные на первом уровне, а также ячейки, в которые будут теперь записываться серии из исходной последовательности.
80 Фактически пустые ячейки представляют собой пустые серии, которые замещаются реальными сериями при считывании их из исходной последовательности Рис. 4.5. Горизонтальное распределение серий, L = 2 Введем переменные
a
i
и
d
i
, отражающие идеальное число серий и число пустых серий для последовательности
i. Вначале работы алгоритма горизонтального распределения эти переменные инициализируются следующим образом
a
i
= 1,
d
i
= 1, для
i = 1, …, N – 1. Эти значения соответствуют рис. 4.4. Значения
a
i
приведены в табл. 4.4 и изменяются в соответствии с уровнем
L. Значения d
i
определяются как разность
a
i
текущего и предыдущего уровней
d
i
=
a
i
(L)
–
a
i
(L – 1)
. На рис. 4.6 приведены значения переменных и
d
i
при переходе на второй уровень.
d
i
1 1 1 1 0
a
i
2 2 2 2 1 1 2 3 4 5 Рис. 4.6. Горизонтальное распределение серий при переходе на второй уровень По мере того, как считываются реальные серии из исходной последовательности, замещая пустые серии входных последовательностей, меняются значения
d
i
(рис. 4.7, 4.8). Переход на третий уровень (
L = 3) приведет к изменению значений переменных a
i
и
d
i
, показанному на рис. 4.9. Поскольку пустые серии должны быть распределены по последовательностям равномерно, то десятая серия исходной последовательности будет записана не в
f
5
(что привело быка встанет равным 1). Запись десятой серии показана на рис. 4.11. Аналогично, одиннадцатая и двенадцатая серии будут записаны в
f
2 и
f
3
. После этого запись серий будет происходить поочередно, начиная с по
f
5
(рис. 4.11, 4.12).
81
d
i
0 1 1 1 0
a
i
2 2 2 2 1 6
1 2 3 4 5 Рис. 4.7. Горизонтальное распределение при считывании шестой серии
d
i
0 0 0 0 0
a
i
2 2 2 2 1 6 7 8 9 1 2 3 4 5 Рис. 4.8. Горизонтальное распределение при считывании девятой серии
d
i
2 2 2 1 1
a
i
4 4 4 3 2 6 7 8 9 1 2 3 4 5 Рис. 4.9. Горизонтальное распределение серий при переходе на третий уровень
d
i
1 2 2 1 1
a
i
4 4 4 3 2 10 6 7 8 9 1 2 3 4 5 Рис. 4.10. Горизонтальное распределение при считывании десятой серии
82 Состояние последовательностей и переменных
a
i
и
d
i
при переходе на четвертый уровень показан на рис. 4.13.
d
i
0 1 1 1 1
a
i
4 4 4 3 2 13 10 11 12 6 7 8 9 1 2 3 4 5 Рис. 4.11. Горизонтальное распределение при считывании тринадцатой серии
d
i
0 0 0 0 0
a
i
4 4 4 3 2 13 14 15 10 11 12 16 6 7 8 9 17 1 2 3 4 5 Рис. 4.12. Горизонтальное распределение при считывании семнадцатой серии
d
i
4 4 3 3 2
a
i
8 8 7 6 4 13 14 15 10 11 12 16 6 7 8 9 17 1 2 3 4 5 Рис. 4.13. Горизонтальное распределение серий при переходе на четвертый уровень Замещение пустых серий реальными происходит также с учетом равномерного распределения пустых серий по всем последовательностям. Порядок записи серий на четвертом уровне (
L = 4) показан на рис. 4.14.
83 Таким образом происходит заполнение входных последовательностей, пока в исходной последовательности не закончатся все серии. В конце распределения во входных последовательностях будут содержатся реальные и пустые серии, сумма которых представляет собой одно из чисел Фибоначчи порядка
N – 2, что идеально подходит для многофазной сортировки 27 13 14 15 23 33 10 11 12 16 28 6 7 8 9 17 1 2 3 4 5 Рис. 4.14. Горизонтальное распределение серий в конце четвертого уровня При повышении уровня при горизонтальном распределении серий можно представить
d
i
, как цель, которую можно достичь, если направить серий в последовательность
i. Можно считать, что в последовательность
i сразу помещается d
i
пустых серий. Последующее распределение рассматривается, как замена пустых серий на реальные, отмечая каждую замену вычитанием 1 из переменной. Таким образом, по исчерпании исходной последовательности
d
i
будет указывать число пустых серий в последовательности
i. В общем виде алгоритм горизонтального распределения серий можно записать следующим образом
1. Цель – получить при распределении по одной серии в каждой последовательности (при распределении используются числа Фибоначчи порядка
N – 2).
2. Проводим распределение, стремясь достичь цели.
3. Если цели невозможно достичь из-за того, что серии исходной последовательности исчерпаны, заканчиваем распределение.
4. Если цель же достигнута, вычисляем следующий уровень чисел Фибоначчи. Разность между этими числами и числами на предыдущем уровне (
d
i
=
a
i
(L)
–
a
i
(L – 1)
) становится новой целью распределения. Возвращаемся к шагу 2.
84 Следует отметить, что для выходной последовательности
a
N
= 0,
d
N
= 0. После того, как был рассмотрен пример и сформулирован алгоритм распределения серий, можно написать код функции
select(), обращение к которой происходит всякий раз, когда необходимо выбрать, в какую последовательность записывать новую серию. Функция
select() должна также вычислять новую строку табл. 4.4, те. значения
a
1
(L)
, …,
a
N –1
(L)
, и новые цели
d
i
=
a
i
(L)
–
a
i
(L – 1)
. Листинг тела функции
select() приведен ниже
int i, a0;
if ((d[j]) < (d[j + 1])) j++;
else
{
if (d[j] == 0)
{
L++;
a0 = a[0];
for(i = 0; i < (N – 1); i++)
{
d[i] = a0 + a[i + 1] – a[i];
a[i] = a0 + a[i + 1];
}
}
j = 0;
}
d[j] = d[j] – 1; Переменная
j представляет собой индекс входной последовательности. Ее первоначальное значение равно нулю. Если исходной последовательностью является
f
0
, тогда начальное распределение серий может быть сформулировано следующим образом пока (не конец последовательности
f
0
)
{ вызвав функцию select(), определить, куда записать серию записать серию изв Однако при распределении серий во входные последовательности следует учесть, что, как и при записи серий в ранее разбиравшейся сортировке естественным слиянием, возможно объединение двух последовательно поступающие в одну последовательность серий. А это приведет к
85 нарушению ожидаемого числа серий. В многофазной сортировке особенно важно точное число серий в каждой последовательности. Поэтому во избежание такого случайного слияния приходится усложнять алгоритм распределения. Нужно для всех последовательностей хранить значение последнего элемента последней серии. Тогда алгоритм распределения может выглядеть так пока (не конец последовательности
f
0
)
{ вызвав функцию select(), определить, куда записать серию если (последний элемент
f
j
больше первого элемента
f
0
), тогда записать серию изв иначе записать серию изв она объединится с последней серией
f
j
если (последовательность
f
0
не закончилась, тогда записать еще одну серию изв и это будет действительно новая серия
f
j
иначе
d[j] = d[j] + 1; // d[j] необходимо увеличить на 1, так как в
// последней строке функции
select() он
// был уменьшен на 1, и теперь это
// нужно компенсировать.
} Поскольку вначале работы алгоритма распределения серий входные последовательности пусты, то и значения их последних элементов не определено. Поэтому следует сначала скопировать по одной серии в каждую последовательность, и затем уже применять только что рассмотренный алгоритм. Алгоритм многофазной сортировки При обсуждении чисел Фибоначчи мы уже коснулись самого алгоритма многофазной сортировки. Так, мы знаем, что на каждом проходе серии из
n – 1 входных файлов сливаются в один выходной файл. При этом нет необходимости использовать все серии каждого входного файла. Выходной файл заполняется сериями, количество которых равно минимальному количеству серий среди всех входных файлах. Когда серии какого- либо входного файла заканчиваются, он становится выходным файлом, и начинается новый проход. При этом все остальные файлы становятся входными.
86 Основная структура алгоритма многофазной сортировки подобна главной части программы многопутевого слияния. Она состоит из трех вложенных циклов внешний цикл сливает серии, пока не будут исчерпаны входы, внутренний сливает одиночные серии из каждой входной последовательности, а самый внутренний цикл выбирает элемент с минимальным значением и пересылает его в выходной файл. Таблица 4.6 Отличия многопутевой и многофазной сортировок Параметры
Многопутевая сортировка Многофазная сортировка Число входных последовательностей
N/2
N – 1 Число выходных последовательностей
N/2 1 Переключение входных и выходных последовательностей в конце прохода Входные и выходные последовательности меняются ролями Опустошившаяся последовательность становится выходной, остальные – входными Наличие пустых серий Отсутствуют. Алгоритмом непредусмотрены Присутствуют. Начальное распределение и сортировка без пустых серий невозможны В табл. 4.6 приведены основные отличия многопутевой и многофазной сортировок. При реализации многофазной сортировки для индексации входных и выходных последовательностей, также как и при многопутевой сортировке, используется карта индексов лент
t
i
. Слияние происходит из входных последовательностей
t
1
, …,
t
N –1
в
t
N
. Как только одна из входных последовательностей становится пустой, индексу
t
N
присваивается индекс этой последовательности. Таким образом, индекс
t
N
всегда указывает на выходную последовательность. Разумеется, предыдущее значение индекса
t
N
не теряется, поскольку последовательность, на которую он указывал, теперь становится входной и участвует в слиянии наряду с остальными последовательностями. Для индексации активных входных последовательностей используется карты индексов активных входов
ta. Эта карта содержит индексы последовательностей, которые участвуют в слиянии. Считается, что последовательность участвует в слиянии, если она не содержит пустых серий, и конец текущей рассматриваемой серии еще не наступил. Число входных последовательностей, участвующих в слиянии, или число активных входов, обозначается через
k (k ≤ N – 1).
87 Число активных входных последовательностей меняется от серии к серии. Вначале каждой серии оно определяется по числу пустых серий
d
i
: если для всех, то делаем вид, что сливаем
N – 1 пустых серий и создаем пустую серию на выходе, те. просто увеличиваем
d
N
для выходной последовательности. В противном случае сливается по одной серии из всех входных последовательностей, у которых
d
i
= 0, a
d
i
остальных последовательностей уменьшается на единицу. Это означает, что из них взято по одной пустой серии.
При многофазной сортировке уровень, вычисленный при начальном распределении, уменьшается. При этом перевычисляется и необходимое число серий а последовательности
i. Как только уровень станет равным нулю – сортировка закончена, исходная последовательность отсортирована. В общем виде алгоритм многофазной сортировки можно записать так
do // слияние изв открываем на запись последовательность
t
N
;
do // слияние одной серии
{
k = 0;
for(i = 0; i < (N – 1); i++) // определение числа активных входов
if (d[i] > 0) d[i] = d[i] – l;
else { k++; ta[k] = t[i]; }
if (k == 0) d[N] = d[N] + l;
else слияние одной реальной серии изв закрываем последовательность
t
N
; и открываем ее на чтение поворот карты индексов лент t; вычисление
a[i] для следующего уровня открываем на запись последовательность
t
N
;
L––;
// уменьшаем уровень на единицу
}
while (L > 0); отсортированный результат содержится в
t[0]; Предполагается, что вначале все
N – 1 входных последовательностей с исходными сериями находятся в состоянии чтения, а компоненты карты индексов лент
t
i
=
i.
88 Операция слияния реальной серии почти идентична той же операции в сортировке с помощью многопутевого слияния, разница только в том, что здесь алгоритм исключения последовательности несколько проще
do
{
i = 0; mx = 0; min = первый элемент последовательности с индексом первый элемент последовательности с индексом ta[i];
if (x < min) {min = x; mx = i;}
} скопировать элемент из последовательности с индексом
ta[i] в выходную последовательность, индекс которой
t[N];
if (конец серии в последовательности с индексом ta[mx])
{
ta[mx] = ta[k]; k––; } // исключаем из списка активных входов
}
while (k > 0); Поворот карт индексов последовательностей, соответствующих счетчиков
d
i
, а также перевычисление коэффициентов а при переходе на низший уровень происходит следующим образом
tn = t[N]; dn = d[N]; z = a[N – 1];
for(i = N; i > 1 ; i–– )
{
t[i] = t[i – 1]; d[i] = d[i – 1]; a[i] = a[i – 1] – z; }
t[0] = tn; d[0] = dn; a[0] = z; В заключение приведем листинг программы, реализующей алгоритм многофазной сортировки, в общем виде
int j, a[N], d[N], L;
select()
{
int i, a0;
if ((d[j]) < (d[j + 1])) j++;
else
{
if (d[j] == 0)
{
89
L++;
a0 = a[0];
for(i = 0; i < (N – 1); i++)
{
d[i] = a0 + a[i + 1] – a[i];
a[i] = a0 + a[i + 1];
}
}
j = 0;
}
d[j] = d[j] – 1;
}
main()
{
int i, z, k, x, mx, min;
int t[N], ta[N –1];
FILE *f[N], f0;
for(i = 0; i < (N – 1); i++) открыть входные последовательности
f
i на запись
for(i = 0; i < (N – 1); i++)
{
a[i] = 1; d[i] = 1; }
L = 1; j = 0; a[N] = 0; d[N] = 0; записать по одной серии в каждый из входных файлов пока (не конец последовательности
f
0
) // выполнить начальное
{ // распределение вызвав функцию select(), определить, куда записать серию если (последний элемент
f
j
больше первого элемента
f
0
), тогда записать серию изв иначе записать серию изв если (последовательность
f
0
не закончилась, тогда записать еще одну серию изв иначе
d[j] = d[j] + 1;
}
for(i = 0; i < N; i++)
t[i] = i;
90 открыть входные последовательности
f
i на чтение
do // сортировка
{
z = a[N – l]; d[N] = 0; открываем на запись последовательность
t
N
;
do // слияние одной серии
{
k = 0;
for(i = 0; i < (N – 1); i++)
if (d[i] > 0) d[i] = d[i] – l;
else { k++; ta[k] = t[i]; }
if (k == 0) d[N] = d[N] + l;
else
do // слияние одной реальной серии изв первый элемент файла с индексом ta[0]; while(
i < k)
{
i++; x = первый элемент последовательности с индексом ta[i];
if (x < min) {min = x; mx = i;}
} скопировать элемент из последовательности с индексом
ta[i] в выходную последовательность, индекс которой
t[N];
if (конец серии в последовательности с индексом ta[mx])
{
ta[mx] = ta[k]; k––; }
}
while (k > 0);
z––;
}
while (z > 0); закрываем последовательность
t
N
; и открываем ее на чтение
tn = t[N]; dn = d[N]; z = a[N – 1];
for(i = N; i > 1 ; i–– ) // поворот карты индексов лент t;
{
t[i] = t[i – 1]; d[i] = d[i – 1];
a[i] = a[i – 1] – z; } // вычисление a[i] для следующего уровня
t[0] = tn; d[0] = dn; a[0] = z; открываем на запись последовательность
t
N
;
L––;
// уменьшаем уровень на единицу
}
while (L > 0); вывод на экран или сохранение в файл отсортированной последовательности, которая теперь содержится в
t[0];
}
91 Алгоритм многофазной сортировки гораздо эффективнее рассмотренных ранее алгоритмов многопутевого, естественного и простого слияния. Его применение при сортировке последовательности позволяет значительно сократить время, необходимое для обработки данных. Однако очевидно, что реализация многофазной сортировки требует значительных трудозатрат на кодирование и отладку программного кода. Поэтому применение данной сортировки целесообразно при большом объеме данных и необходимости минимизации времени, затрачиваемого на сортировку. В том случае, если объем данных небольшой, а при современных вычислительных мощностях это может быть до нескольких сот тысяч элементов, рекомендуется использовать более простые методы внешней сортировки. Задания
1. Написать программу внешней сортировки последовательности, методом простого слияния.
2. Написать программу естественного слияния с использованием двух вспомогательных файлов.
3. Написать программу естественного слияния с использованием четырех файлов.
4. Программно реализовать алгоритмы простого и естественного слияния, и сравнить их на эффективность по количеству проходов.
5. Реализовать алгоритм многопутевого слияния с общим числом файлов не менее шести.
6. Написать программу многофазной сортировки последовательности при
N = 3, 4, 5 или 6.
7. Существует метод сортировки, похожий на многофазную сортировку каскадное слияние. При нем слияние идет так если, например, речь идет о шести последовательностях, то, начиная с идеального распределения серий на последовательностях Т, …, Т, сначала проводится пя- типутевое, слияние Т, …, Т на Т, до тех пор пока не станет пустой последовательность Т, затем (Туже не трогается) проводится четырехпуте- вое слияние на Т, затем трехпутевое на Т, двухпутевое на Т и копирование из Т в Т. Следующий проход работает по аналогичной схеме все начинается с пятипутевого слияния на Т и т. д. Хотя такая схема выглядит хуже многофазного слияния, поскольку некоторые последовательности простаивают, и есть просто операции копирования, тем не менее она, что удивительно, оказывается лучше многофазной сортировки при очень больших файлах и шести или более последовательностях. Необходимо написать программу такого каскадного слияния с четко выделенной структурой.
92 8. Допустим, используется трехфайловая многофазная сортировка, причем нам этапе создается файл с r
i
сериями длины
l
i
. Нам этапе требуется одна серия из одного файла и ни одной из двух других. Поясните, почему должно соблюдаться каждое из приведенных ниже условий а)
l
i
=
l
i – 1
+
l
i – 2
для
i ≥ 1, где l
0
и
l
–1
– длины серий в двух первоначально занятых файлах били, что эквивалентно
r
i – 2
=
r
i – 1
+
r
i
) для
i ≥ 1, где
r
0
и
r
–1
– количество серий в двух первоначально занятых файлах в)
r
n
=
r
n – 1
= 1 и, следовательно,
r
n
,
r
n – 1
, …,
r
1
образуют последовательность чисел Фибоначчи.
9. Какое дополнительное условие нужно добавить к условиям, перечисленным в упражнении 8, чтобы сделать возможным выполнение многофазной сортировки ас начальными сериями длиной 1 те б) в течение
k этапов, нос другими начальными сериями. Рекомендуется рассмотреть несколько вариантов, например,
l
n
= 50,
l
n
– 1
= 31 или
l
n
= 50,
l
n – 1
= 32.
10. Покажите, что а) для сортировки
n записей помощью любого алгоритма внешней сортировки, который использует только одну магнитную ленту в качестве устройства внешней памяти, потребуется времени порядка Об) в случае использования двух магнитных лент в качестве устройств внешней памяти достаточно будет
O(n logn) времени.
11. Некоторые операционные системы предоставляют программистам виртуальную память можно писать программы, исходя из предположения, что имеется очень большая внутренняя память, способная вместить все данные. Эта память разделена на страницы, и лишь немногие из них действительно находятся во внутренней памяти в любой момент времени, остальные же хранятся на дисках. Программисту ненужно вникать вовсе эти подробности, так как операционная система сама обо всем заботится необходимые страницы автоматически вызываются в память, когда они нужны. Может показаться, что появление виртуальной памяти приводит к отмиранию методов внешней сортировки, так как эта работа может быть выполнена просто с помощью методов, разработанных для внутренней сортировки. Проанализируйте сложившуюся ситуацию. Почему специально разработанный метод внешней сортировки может оказаться лучше применения общей техники подкачки страниц к методу внутренней сортировки
93
5. ОРИЕНТИРОВАННЫЕ ГРАФЫ Во многих задачах, встречающихся в компьютерных науках, математике, технических дисциплинах часто возникает необходимость наглядного представления отношений между какими-либо объектами. Ориентированные и неориентированные графы – естественная модель для таких отношений. В этой главе рассмотрены основные структуры данных, которые применяются для представления ориентированных графов, а также описаны некоторые основные алгоритмы определения связности ориентированных графов и нахождения кратчайших путей.
5.1. Основные определения Ориентированный графили сокращенно орграф)
G = (V, Е) состоит из множества вершин
V и множества дуг Е. Вершины также называют узлами, а дуги – ориентированными ребрами. Дуга представима в виде упорядоченной пары вершин (
v, w), где вершина v называется началом, a w – концом дуги. Дугу (
v, w) часто записывают как v → w и изображают в виде Говорят также, что дуга
v → w ведет от вершины v к вершине w, а вершина
w смежная с вершиной v. На рис. 5.1 показан орграф с четырьмя вершинами и пятью дугами. Рис. 5.1. Ориентированный граф Вершины орграфа можно использовать для представления объектов, а дуги – для отношений между объектами. Например, вершины орграфа могут представлять города, а дуги – маршруты рейсовых полетов самолетов из одного города в другой. В другом случаев виде орграфа может быть представлена блок-схема потока данных в компьютерной программе. В последнем примере вершины соответствуют блокам операторов программы, а дугам – направленное перемещение потоков данных.
v
w
1 2
3 4
94 Путем в орграфе называется последовательность вершин
v
1
,
v
2
, …, для которой существуют дуги
v
1
→
v
2
,
v
2
→
v
3
, …,
v
n – 1
→
v
n
. Этот путь начинается в вершине
v
1
и, проходя через вершины
v
2
,
v
3
, …,
v
n – 1
, заканчивается в вершине
v
n
. Длина пути – это количество дуг, составляющих путь, в данном случае длина пути равна
n – 1. Как особый случай пути рассмотрим одну вершину
v как путь длины 0 от вершины v к этой же вершине v. На рис. 5.1 последовательность вершин 1, 2, 4 образуют путь длины 2 от вершины 1 к вершине 4. Путь называется простым, если все вершины на нем, за исключением, может быть, первой и последней, различны. Цикл – это простой путь длины не менее 1, который начинается и заканчивается водной и той же вершине. На рис. 5.1 вершины 3, 2, 4, 3 образуют цикл длины 3. Во многих приложениях удобно к вершинами дугам орграфа присоединить какую-либо информацию. Для этих целей используется помеченный орграф, те. орграфу которого каждая дуга и/или каждая вершина имеет соответствующие метки. Меткой может быть имя, весили стоимость дуги, или значение данных какого-либо заданного типа. На рис. 5.2 показан помеченный орграф, в котором каждая дуга имеет буквенную метку. Этот орграф имеет интересное свойство любой цикл, начинающийся в вершине 1, порождает последовательность буква ив которой всегда четное количество буква и b. Рис. 5.2. Помеченный орграф В помеченном орграфе вершина может иметь как имя, таки метку. Часто метки вершин используются в качестве имен вершин. Так, на рис. 5.2 числа, помещенные в вершины, можно интерпретировать как имена вершин и как метки вершин.
1 2
4 3
a
a
a
a
b
b
b
b
95
5.2. Представления ориентированных графов Для представления ориентированных графов можно использовать различные структуры данных. Выбор структуры данных зависит от операторов, которые будут применяться к вершинами дугам орграфа. Одним из наиболее общих представлений орграфа
G = (V, Е) является матрица смежности. Предположим, что множество вершин
V = {1, 2, …, n}. Матрица смежности для орграфа
G – это матрица А размера n x n со значениями бу- левого типа, где
A[i, j] = true тогда и только тогда, когда существует дуга из вершины
i в вершину j. Часто в матрицах смежности значение true заменяется на 1, а значение
false – на 0. Время доступа к элементам матрицы смежности зависит от размеров множества вершин и множества дуг. Представление орграфа в виде матрицы смежности удобно применять в тех алгоритмах, в которых надо часто проверять существование данной дуги. Обобщением описанного представления орграфа с помощью матрицы смежности можно считать представление помеченного орграфа также посредством матрицы смежности, ноу которой элемент
A[i, j] равен метке дуги
i → j. Если дуги от вершины i к вершине j не существует, то значение
A[i, j] не может быть значением какой-либо допустимой метки, а может рассматриваться как пустая ячейка. На рис. 5.3 показана матрица смежности для помеченного орграфа, приведенного на рис. 5.2. Здесь дуги представлены меткой символьного типа, а пробел используется при отсутствии дуг. Основной недостаток матриц смежности заключается в том, что она требует
O(n
2
) объема памяти, даже если дуг значительно меньше, чем Поэтому для чтения матрицы или нахождения в ней необходимого элемента требуется время порядка
O(n
2
), что не позволяет создавать алгоритмы с временем
O(n) для работы с орграфами, имеющими порядка O(n) дуга а b
3
b а
4
b a Рис. 5.3. Матрица смежности для помеченного орграфа, изображенного на рис. 5.2 Поэтому вместо матриц смежности часто используется другое представление для орграфа
G = (V, Е, называемое представлением посредством списков смежности. Списком смежности для вершины
i называется список всех вершин, смежных с вершиной
i, причем определенным образом упорядоченный. Таким образом, орграф
G можно представить посредством массива
HEAD (заголовок, чей элемент HEAD[i] является указателем на список смежности вершины
i. Представление орграфа с помощью списков смежности требует для хранения объем памяти, пропорциональный сумме количества вершин и количества дуг. Если количество дуг имеет порядок
O(n), то и общий объем необходимой памяти имеет такой же порядок. Но и для списков смежности время поиска определенной дуги может иметь порядок
O(n), так как такой же порядок может иметь количество дугу определенной вершины. На рис. 5.4 показана структура данных, представляющая орграф из рис. 5.1 посредством связанных списков смежности. Если дуги имеют метки, то их можно хранить в ячейках связанных списков.
HEAD Списки смежности Рис. 5.4. Структура списков смежности для орграфа, изображенного на рис. 5.1
HEAD
ADJ Рис. 5.5. Представление орграфа, изображенного на рис. 5.1 Для вставки и удаления элементов в списках смежности необходимо иметь массив
HEAD, содержащий указатель на ячейки заголовков списков
2 3
#
2
#
4
#
3
#
1 2
3 4
1 2
3 4
1 2
4 4
3 0
2 3
5 0
8 3
7 0
6 2
9 0
97 смежности, ноне сами смежные вершины. Но если известно, что граф не будет подвергаться изменениям (или они будут незначительны, то предпочтительно сделать массив
HEAD массивом курсоров на массив ADJ (от
adjacency – смежность, где ячейки ADJ[HEAD[i]], ADJ[HEAD[i] + 1] и т. д. содержат вершины, смежные с вершиной
i, и эта последовательность смежных вершин заканчивается первым встреченным нулем в массиве
ADJ. Пример такого представления для графа из рис. 5.1 показан на рис. 5.5.
5.3. Задача нахождения кратчайшего пути В этом параграфе рассматриваются задачи нахождения путей в ориентированном графе. Пусть есть ориентированный граф
G = (V, Е, у которого все дуги имеют неотрицательные метки (стоимости дуга одна вершина определена как источник. Задача состоит в нахождении стоимости кратчайших путей от источника ко всем другим вершинам графа
G (здесь длина пути определяется как сумма стоимостей дуг, составляющих путь. Эта задача часто называется задачей нахождения кратчайшего пути с одним источником. Отметим, что мы будем говорить о длине пути даже тогда, когда она измеряется в других, нелинейных, единицах измерения, например во временных единицах. Можно представить орграф
G в виде карты маршрутов рейсовых полетов из одного города в другой, где каждая вершина соответствует городу, аду- га
v → w – рейсовому маршруту из города v в город w. Метка дуги v → w – это время полета из города
v в город w
2
. В этом случае решение задачи нахождения кратчайшего пути с одним источником для ориентированного графа трактуется как минимальное время перелетов между различными городами. Для решения поставленной задачи будем использовать жадный алгоритм, который часто называют алгоритмом Дейкстры (
Dijkstra). Алгоритм строит множество
S вершин, для которых кратчайшие пути от источника уже известны. На каждом шаге к множеству
S добавляется та из оставшихся вершин, расстояние до которой от источника меньше, чем для других оставшихся вершин. Если стоимости всех дуг неотрицательны, то можно быть уверенным, что кратчайший путь от источника к конкретной
1
Может показаться, что более естественной задачей будет нахождение кратчайшего пути от источника к определенной вершине назначения. Но эта задача в общем случае имеет такой же уровень сложности, что и задача нахождения кратчайшего пути для всех вершин графа (за исключением того счастливого случая, когда путь к вершине назначения будет найден ранее, чем просмотрены пути ко всем вершинам графа.
2
Можно предположить, что в данном случаев качестве модели больше подходит неориентированный граф, поскольку метки дуги могут совпадать. Но фактически в большинстве случаях время полетав противоположных направлениях между двумя городами различно. Кроме того, предположение о совпадении меток дуги не влияет (и не помогает) на решение задачи нахождения кратчайшего пути.
98 вершине проходит только через вершины множества
S. Назовем такой путь особым. На каждом шаге алгоритма используется также массив
D, в который записываются длины кратчайших особых путей для каждой вершины. Когда множество
S будет содержать все вершины орграфа, те. для всех вершин будут найдены особые пути, тогда массив
D будет содержать длины кратчайших путей от источника к каждой вершине. Ниже представлен листинг программы, реализующей алгоритм Дейкст- ры. Здесь предполагается, что в орграфе
G вершины поименованы целыми числами, те. множество вершин
V = {1, 2, …, n}, причем вершина 1 является источником. Массив С – это двумерный массив стоимостей, где элемент С, j] равен стоимости дуги
i → j. Если дуги i → j не существует, то C[i, j] принимается равным ∞, те. большим любой фактической стоимости дуг. На каждом шаге
D[i] содержит длину текущего кратчайшего особого пути к вершине i.
procedure Dijkstra;
begin
(1)
S := {1};
(2)
for i := 2 to n do
(3)
D[i] := C[1, i]; { инициализация D }
(4)
for i := 1 to n – 1 do begin
(5) выбор из множества
V \ S такой вершины w, что значение
D[w] минимально
(6) добавить
w к множеству S;
(7)
for каждая вершина v из множества V \ S do
(8)
D[v] := min(D[v], D[w] + C[w, v])
end
end; Применим алгоритм Дейкстры для ориентированного графа, показанного на рис. 5.6. Рис. 5.6. Орграф с помеченными дугами Вначале
S = {1}, D[2] = 10, D[3] = ∞, D[4] = 30 и D[5] = 100. На первом шаге цикла (строки (4) – (8) листинга)
w = 2, те. вершина 2 имеет минимальное значение в массиве
D. Затем вычисляем D[3] = min(∞, 10 + 50) = 60.
D[4] и D[5] не изменяются, так как не существует дуг, исходящих из вершины и ведущих к вершинами. Последовательность значений элементов массива
D после каждой итерации цикла показаны в табл. 5.1. Несложно внести изменения в алгоритм так, чтобы можно было определить сам кратчайший путь (те. последовательность вершин) для любой вершины. Для этого надо ввести еще один массив Р вершин, где Р содержит вершину, непосредственно предшествующую вершине
v в кратчайшем пути. Вначале положим
P[v] = 1 для всех v ≠ 1. В приведенном выше листинге после строки (8) надо записать условный оператор сусло- вием
D[w] + C[w, v] < D[v], при выполнении которого элементу P[v] присваивается значение
w. После выполнения алгоритма кратчайший путь к каждой вершине можно найти с помощью обратного прохождения по предшествующим вершинам массива Р. Таблица 5.1 Вычисления по алгоритму Дейкстры для орграфа из рис. 5.6 Итерация
S w
D[2]
D[3]
D[4]
D[5] Начало {1} –
10
∞ 30 100 1
{1,
2} 2 10 60 30 100 2
{1, 2, 4}
4 10 50 30 90 3
{1, 2, 4, 3}
3 10 50 30 60 4
{1, 2, 4, 3, 5}
5 10 50 30 60 Определим кратчайший путь на примере орграфа, показанного на рис. 5.6. Массив Р имеет следующие значения Р = 1, Р = 4, Р = 1, Р = 3. Для определения кратчайшего пути, например, от вершины 1 к вершине 5, надо отследить в обратном порядке предшествующие вершины, начиная с вершины 5. На основании значений массива Р определяем, что вершине 5 предшествует вершина 3, вершине 3 – вершина 4, а ей, в свою очередь, – вершина 1. Таким образом, кратчайший путь из вершины 1 в вершину 5 составляет следующая последовательность вершин 1, 4, 3, 5. Обоснование алгоритма Дейкстры Алгоритм Дейкстры – пример алгоритма, где жадность окупается в том смысле, что если что-то хорошо локально, то оно будет хорошо и глобально. В данном случае что-то локально хорошее – это вычисленное расстояние от источника к вершине
w, которая пока не входит в множество, но имеет кратчайший особый путь. (Напомним, что особым называется путь, который проходит только через вершины множества
S.) Чтобы понять, почему не может быть другого кратчайшего, ноне особого, пути, рассмотрим рис. 5.7. Здесь показан гипотетический кратчайший путь к вершине
w, который сначала проходит до вершины х через вершины
100 множества
S, затем после вершины х путь, возможно, несколько раз входит в множество
S и выходит из него, пока не достигнет вершины w. Но если этот путь короче кратчайшего особого пути к вершине
w, то и начальный сегмент пути от источника к вершине х (который тоже является особым путем) также короче, чем особый путь к
w. Нов таком случаев строке листинга (5) при выборе вершины
w необходимо было выбрать не эту вершину, а вершину х, поскольку D[x] меньше D[w]. Таким образом, приходим к противоречию, следовательно, не может быть другого кратчайшего пути к вершине
w, кроме особого. (Отметим здесь определяющую роль того факта, что все стоимости дуг неотрицательны, без этого свойства помеченного орграфа алгоритм Дейкстры не будет работать правильно) Для завершения обоснования алгоритма Дейкстры надо еще доказать, что
D[v] действительно показывает кратчайшее расстояние доверши- ны
v. Рассмотрим добавление новой вершины w к множеству S (строка листинга (6)), после чего происходит пересчет элементов массива
D в строках (7), (8) этого листинга при этом может получиться более короткий особый путь к некоторой вершине
v, проходящий через вершину w. Если часть этого пути до вершины
w проходит через вершины предыдущего множества
S и затем непосредственно к вершине v, то стоимость этого пути, в строке листинга (8) сравнивается си, если новый путь короче, изменяется значение
D[v]. Существует еще одна гипотетическая возможность кратчайшего особого пути к вершине
v, которая показана на рис. 5.8. Рис. 5.7. Гипотетический кратчайший путь к вершине Рис. 5.8. Реально невозможный кратчайший особый путь
Здесь этот путь сначала идет к вершине
w, затем возвращается к вершине х, принадлежащей предыдущему множеству S, затем следует к вершине
v. Но реально такого кратчайшего путине может быть. Поскольку вершинах помещена в множество S раньше вершины w, то все кратчайшие пути от источника к вершине х проходят исключительно через вершины предыдущего множества
S. Поэтому показанный на рис. 5.8 путь к вершине, проходящий через вершину w, не короче, чем путь к вершине х, про
w Множество S Источник
w
x Множество S
Источник
101 ходящий через вершины множества
S. В результате и весь путь к вершине v, проходящий через вершины хине короче, чем путь от источника к вершине х, проходящий через вершины множества S, и далее непосредственно к вершине
v. Таким образом, доказано, что оператор в строке листинга) действительно вычисляет длину кратчайшего пути. Время выполнения алгоритма Дейкстры Предположим, что алгоритм Дейкстры оперирует с орграфом, имеющим
n вершин и е дуг. Если для представления орграфа используется матрица смежности, то для выполнения внутреннего цикла строки) потребуется время
O(n), а для выполнения всех n – 1 итераций цикла строки) потребуется время порядка
O(n
2
). Время, необходимое для выполнения оставшейся части алгоритма, как легко видеть, не превышает этот же порядок. Если количество дуге значительно меньше n
2
, то лучшим выбором для представления орграфа будут списки смежности, а для множества вершин
V \ S – очередь с приоритетами, реализованная в виде частично упорядоченного дерева. Тогда время выбора очередной вершины из множества и пересчет стоимости путей для одной дуги составит O(log n), а общее время выполнения цикла строки е log n), а не O(n
2
). Строки (1) – (3) выполняются за время порядка
O(n). При использовании очередей с приоритетом для представления множества
V \ S строка
(5) реализуется посредством оператора
DELETEMIN, а каждая из n – 1 итераций цикла (4) – (6) требует времени порядка
O(log n). В результате получаем, что общее время выполнения алгоритма
Дейкстры ограничено величиной порядка е log n). Это время выполнения значительно меньше, чем
O(n
2
), когда е существенно меньше п. Нахождение кратчайших путей между парами вершин Предположим, что имеется помеченный орграф, который содержит время полета по маршрутам, связывающим определенные города. Необходимо построить таблицу, где приводилось бы минимальное время перелета из одного (произвольного) города в любой другой. В этом случае мы сталкиваемся с общей задачей нахождения кратчайших путей, те. нахождения кратчайших путей между всеми парами вершин орграфа. Более строгая формулировка этой задачи следующая есть ориентированный граф
G = (V, Е, каждой дуге
v → w этого графа сопоставлена неотрицательная стоимость С, w]. Общая задача нахождения кратчайших путей заключается в нахождении для каждой упорядоченной пары вершин (
v, w) любого пути от
102 вершины
v в вершины w, длина которого минимальна среди всех возможных путей от
v к w. Можно решить эту задачу, последовательно применяя алгоритм
Дейкстры для каждой вершины, объявляемой в качестве источника. Но существует прямой способ решения данной задачи, использующий алгоритм Флойда (
R. W. Floyd). Для определенности положим, что вершины графа последовательно пронумерованы от 1 до
n. Алгоритм Флойда использует матрицу А размерах, в которой вычисляются длины кратчайших путей. Вначале
A[i, j] = C[i, j] для всех i ≠ j. Если дуга i → j отсутствует, то
C[i, j] = ∞. Каждый диагональный элемент матрицы А равен 0. Над матрицей А выполняется n итераций. После й итерации A[i, j] содержит значение наименьшей длины путей из вершины
i в вершину j, которые не проходят через вершины с номером, большим
k. Другими словами, между концевыми вершинами пути
i и j могут находиться только вершины, номера которых меньше или равны
k. На й итерации для вычисления матрицы А применяется следующая формула
A
k
[
i, j] = min(A
k – 1
[
i, j], A
k – 1
[
i, k] + A
k – 1
[
k, j]). Нижний индекс
k обозначает значение матрицы А после й итерации. Но это не означает, что существует
n различных матриц, этот индекс используется для сокращения записи. Графическая интерпретация этой формулы показана на рис. 5.9. Рис. 5.9. Включение вершины k в путь от вершины i к вершине j Для вычисления
A
k
[
i, j] проводится сравнение величины A
k – 1
[
i, j] те. стоимость пути от вершины
i к вершине j безучастия вершины k или другой вершины с более высоким номером) с величиной
A
k – 1
[
i, k] + A
k – 1
[
k, j] стоимость пути от вершины
i до вершины k плюс стоимость пути от вершины до вершины j). Если путь через вершину k дешевле, чем A
k – 1
[
i, j], то величина
A
k
[
i, j] изменяется.
k
j
A
k – 1
[k, j]
i
A
k – 1
[i, k]
A
k – 1
[i, j]
103 На рис. 5.10 показан помеченный орграфа на рис. 5.11 – значения матрицы А после трех итераций. Рис. 5.10. Помеченный ориентированный граф
1 2 3 1 2 3 1
0 8 5 1
0 8 5 2 3 0
∞ 2 3
0 8
3
∞ 2 0 3
∞ 2 0
A
0
[i, j]
A
1
[i, j]
1 2 3 1 2 3 1
0 8 5 1
0 7 5 2
3 0 8 2
3 0 8 3
5 2 0 3
5 2 0
A
2
[i, j]
A
3
[i, j] Рис. 5.11. Последовательные значения матрицы А Равенства
A
k
[
i, k] = A
k – 1
[
i, k] и A
k
[
k, j] = A
k – 1
[
k, j] означают, что на й итерации элементы матрицы А стоящие в й строке им столбце, не изменяются. Более того, все вычисления можно выполнить с применением только одной копии матрицы А. Время выполнения программы, реализующая алгоритм Флойда, имеет порядок
O(n
3
). Доказательство правильности работы этого алгоритма выполняется с помощью математической индукции по
k, показывая, что на й итерации вершина k включается в путь только тогда, когда новый путь короче старого. Сравнение алгоритмов Флойда и Дейкстры Поскольку версия алгоритма Дейкстры с использованием матрицы смежности находит кратчайшие пути от одной вершины за время порядка п, тов этом случае применение алгоритма Дейкстры для нахождения всех кратчайших путей потребует времени порядка
O(n
3
), те. получим такой же временной порядок, как ив алгоритме Флойда. Константы пропорциональности в порядках времени выполнения для обоих алгоритмов зависят от применяемых компилятора и вычислительной машины, а также от
104 особенностей реализации алгоритмов. Вычислительный эксперимент и измерение времени выполнения – самый простой путь подобрать лучший алгоритм для конкретного приложения. Если е, количество дуг в орграфе, значительно меньше, чем п, тогда, несмотря на относительно малую константу в выражении порядка
O(n
3
) для алгоритма Флойда, рекомендуется применять версию алгоритма Дейк- стры со списками смежности. В этом случае время решения общей задачи нахождения кратчайших путей имеет порядок пе log n), что значительно лучше алгоритма Флойда, по крайней мере, для больших разреженных графов. Вывод на печать кратчайших путей
Во многих ситуациях требуется указать сам кратчайший путь от одной вершины к другой. Чтобы восстановить при необходимости кратчайшие пути в алгоритме Флойда можно ввести еще одну матрицу Р, в которой элемент
P[i, j] содержит вершину k, полученную при нахождении наименьшего значения
A[i, j]. Если P[i, j] = 0, то кратчайший путь из вершины
i в вершину j состоит из одной дуги i → j. Для вывода на печать последовательности вершин, составляющих кратчайший путь от вершины
i до вершины j, вызывается процедура path(i, j):
procedure path (i, j: integer);
var
k: integer;
begin
k := P[i, j];
if k = 0 then
return;
path (i, k);
writeln(k);
path (k, j);
end; На рис. 5.12 показана результирующая матрица Р для орграфа из рис. 5.10.
1 2
3 1 0 3
0 2 0 0
1 3 2 0
0
P Рис. 5.12. Матрица Р для орграфа, изображенного на рис. 5.10
105 Транзитивное замыкание
Во многих задачах интерес представляет только сам факт существования пути, длиной не меньше единицы, от вершины
i до вершины j. Алгоритм Флойда можно приспособить для решения таких задач. Но полученный в результате алгоритм еще до Флойда разработал Уоршелл (
S. War-
shall), поэтому будем называть его алгоритмом Уоршелла. Предположим, что матрица стоимостей С совпадает с матрицей смежности для данного орграфа
G, те. C[i, j] = 1 только в том случае, если есть дуга
i → j, и C[i, j] = 0, если такой дуги не существует. Необходимо вычислить матрицу Атакую, что A[i, j] = 1 тогда и только тогда, когда существует путь от вершины
i до вершины j длиной не менее 1 ив противном случае. Такую матрицу А часто называют транзитивным замыканием матрицы смежности. На рис. 5.13 показано транзитивное замыкание матрицы смежности орграфа из рис. 5.10.
1 2
3 1 1 1
1 2 1 1
1 3 1 1
1 Рис. 5.13. Матрица Р для орграфа, изображенного на рис. 5.10 Транзитивное замыкание можно вычислить, применяя нам шаге следующую формулу к булевой матрице А
A
k
[
i, j] = A
k – 1
[
i, j]
or (A
k – 1
[
i, k] and A
k – 1
[
k, j]). Эта формула устанавливает, что существует путь от вершины
i до вершины
j, проходящий через вершины с номерами, не превышающими k, только в следующих случаях
1. Уже существует путь от вершины
i до вершины j, который проходит через вершины с номерами, не превышающими
k – 1.
2. Существует путь от вершины
i до вершины k, проходящий через вершины с номерами, не превышающими
k – 1, и путь от вершины k до вершины
j, который также проходит через вершины с номерами, не превышающими. Здесь
A
k
[
i, k] = A
k – 1
[
i, k] и A
k
[
k, j] = A
k – 1
[
k, j], и вычисления можно выполнять водной копии матрицы А Программа Warshall вычисления транзитивного замыкания показана в следующем листинге
procedure Warshall (var A: array[1..n, 1..n] of boolean;
C: array[1..n, 1..n] of boolean);
var
106
i, j, k: integer;
begin
for i := 1 to n do
for j
:= 1
to n do
A[i, j] := C[i, j];
for k: = 1 to n do
for i := 1 to n do
for j := 1 to n do
if A[i, j] = false then
A[i, j] := A[i, k] and A[k, j]
end; Нахождение центра ориентированного графа
Предположим, что необходимо найти центральную вершину в орграфе. Эту задачу также можно решить с помощью алгоритма Флойда. Однако сначала уточним термин центральная вершина. Пусть
v – произвольная вершина орграфа
G = (V, Е).Эксцентриситет или максимальное удаление вершины
v определяется как mах{минимальная длина пути от вершины
w до вершины v}
w
V Центром орграфа G называется вершина с минимальным эксцентриситетом. Другими словами, центром орграфа является вершина, для которой максимальное расстояние (длина пути) до других вершин минимально. Рассмотрим помеченный орграф, показанный на рис. 5.14. В этом графе вершины имеют следующие эксцентриситеты: Вершина Эксцентриситет
a
∞
b
6
c
8
d
5
e
7 Найти центр орграфа сравнительно просто. Пусть С – матрица стои- мостей для орграфа
G.
1. Сначала применим алгоритм Флойда к матрице С для вычисления матрицы А, содержащей все кратчайшие пути орграфа G.
2. Находим максимальное значение в каждом столбце
i матрицы А Это значение равно эксцентриситету вершины
i.
3. Находим вершину с минимальным эксцентриситетом. Она и будет центром графа
G.
107 Время выполнения этого процесса определяется первым шагом, для которого время имеет порядок
O(n
3
). Второй шаг требует времени порядка па третий – п. Рис. 5.14. Помеченный граф Матрица всех кратчайших путей для орграфа, изображенного на рис.
5.14 представлена на рис. 5.15.
a b c d e
a
0 1 3 5 7
b
∞ 0 2 4 6
c
∞ 3 0 2 4
d
∞ 1 3 0 7
e
∞ 6 8 5 0 max
∞ 6 8 5 7 Рис. 5.15. Матрица кратчайших путей Максимальные значения в каждом столбце приведены под матрицей.
5.5. Обход ориентированных графов Для решения многих задач, касающихся ориентированных графов, необходим эффективный метод систематического обхода вершин и дуг орграфов. Таким методом является так называемый поиск в глубину. Метод поискав глубину составляет основу многих других эффективных алгоритмов работы с графами. В последних двух разделах этой главы представлены различные алгоритмы, основанные на методе поискав глубину. Предположим, что есть ориентированный граф
G, в котором первоначально все вершины помечены меткой
unvisited (не посещалась. Поиск
108 в глубину начинается с выбора начальной вершины
v графа G, для этой вершины метка
unvisited меняется наметку (посещалась. Затем для каждой вершины, смежной с вершиной
v и которая не посещалась ранее, рекурсивно применяется поиск в глубину. Когда все вершины, которые можно достичь из вершины
v, будут посещены, поиск заканчивается. Если некоторые вершины остались непосещенными, то выбирается одна из них и поиск повторяется. Этот процесс продолжается до тех пор, пока обходом не будут охвачены все вершины орграфа
G. Этот метод обхода вершин орграфа называется поиском в глубину, поскольку поиск непосещенных вершин идет в направлении вперед вглубь) до тех пор, пока это возможно. Например, пусть х – последняя посещенная вершина. Для продолжения процесса выбирается какая-либо не- рассмотренная дугах у выходящая из вершины х Если вершина у уже посещалась, то ищется другая вершина, смежная с вершиной х Если вершина у ранее не посещалась, то она помечается меткой visited и поиск начинается заново от вершины у. Пройдя все пути, которые начинаются в вершине у возвращаемся в вершину х, те. в ту вершину, из которой впервые была достигнута вершина у. Затем продолжается выбор нерассмотрен- ных дуг, исходящих из вершины хи так до тех пор, пока не будут исчерпаны все эти дуги. Для представления вершин, смежных с вершиной
v, можно использовать список смежности
L[v], а для определения вершин, которые ранее посещались, – массив
mark (метка, чьи элементы будут принимать только два значения
visited и unvisited. Эскиз рекурсивной процедуры dfs (от
depth-first search – поиск в глубину, реализующей поиск в глубину, представлен в следующем листинге
procedure dfs (v: вершина
var
w: вершина
begin
(1)
mark[v] := visited;
(2)
for каждая вершина w из списка L[v] do
(3)
if mark[w] = unvisited then
(4)
dfs(w)
end; Чтобы применить эту процедуру к графу, состоящему из п вершин, надо сначала присвоить всем элементам массива
mark значение unvisited, затем начать поиск в глубину для каждой вершины, помеченной как
unvi-
sited. Описанное можно реализовать с помощью следующего кода
109
for v := 1 to n do
mark[v] := unvisited;
for v := 1 to n do
if mark[v] = unvisited then
dfs(v) Отметим, что листинг процедуры поискав глубину является только эскизом, который еще следует детализировать. Заметим также, что эта процедура изменяет только значения массива
mark. Анализ процедуры поискав глубину Все вызовы процедуры
dfs для полного обхода графа с п вершинами и е дугами, если е ≥ п, требуют общего времени порядка е. Чтобы показать это, заметим, что нет вершины, для которой процедура
dfs вызывалась бы больше одного раза, поскольку рассматриваемая вершина помечается как
visited в строке листинга (1) еще до следующего вызова процедуры dfs и никогда не вызывается для вершин, помеченных этой меткой. Поэтому общее время выполнения строки) для просмотра всех списков смежности пропорционально сумме длин этих списков, те. имеет порядок е. Таким образом, предполагая, что е ≥ п, общее время обхода по всем вершинам орграфа имеет порядок е, необходимое для просмотра всех дуг графа. Рассмотрим следующий пример. Пусть процедура
dfs применяется к ориентированному графу, представленному на рис. 5.16, начиная с вершины А Рис. 5.16. Ориентированный граф Алгоритм помечает эту вершину как
visited и выбирает вершину Виз списка смежности вершины А. Поскольку вершина В помечена как unvi-
sited, обход графа продолжается вызовом процедуры dfs(B). Теперь процедура помечает вершину В как visited и выбирает первую вершину из списка смежности вершины В. В зависимости от порядка представления вершин в списке смежности, следующей рассматриваемой вершиной может быть или вершина С, или вершина D. Предположим, что в списке смежности вершина С предшествует вершине
D. Тогда осуществляется вызов dfs(C). В списке смежности вершины С присутствует только вершина А, но она уже посещалась ранее. Поскольку все вершины в списке смежности вершины С исчерпаны, то поиск возвращается в вершину В, откуда процесс поиска продолжается вызовом процедуры
dfs(D). Вершины Аи Сиз списка смежности вершины D уже посещались ранее, поэтому поиск возвращается сначала в вершину В, а затем в вершину А. На этом первоначальный вызов
dfs(A) завершен. Но орграф имеет вершины, которые еще не посещались Е F и G. Для продолжения обхода вершин графа выполняется вызов
dfs(E). Глубинный остовный лес В процессе обхода ориентированного графа методом поискав глубину только определенные дуги ведут к вершинам, которые ранее не посещались. Такие дуги, ведущие к новым вершинам, называются дугами дерева и формируют для данного графа остовный лес, построенный методом поискав глубину, или, сокращенно, глубинный остовный лес. На рис. 5.17 показан глубинный остовный лес для графа из рис. 5.16. Здесь сплошными линиями обозначены дуги дерева. Отметим, что дуги дерева формируют именно лес, те. совокупность деревьев, поскольку методом поискав глубину к любой ранее не посещавшейся вершине можно придти только по одной дуге, а не по двум различным дугам. В добавление к дугам дерева существуют еще три типа дуг, определяемых в процессе обхода орграфа методом поискав глубину. Это обратные, прямые и поперечные дуги. Рис. 5.17. Глубинный остовной лес для орграфа, изображенного на рис 5.16 Обратные дуги (как дуга С → А на рис. 5.17) – это такие дуги, которые в остовном лесе идут от потомков к предкам. Отметим, что дуга из вершины в саму себя также является обратной дугой. Прямыми дугами называются дуги, идущие от предков к истинным потомкам, но которые не являются дугами дерева. На рис. 5.17 прямые дуги отсутствуют.
111 Дуги, такие как
D → C и G → D на рис. 5.17, соединяющие вершины, не являющиеся ни предками, ни потомками друг друга, называются поперечными дугами. Если при построении остовного дерева сыновья одной вершины располагаются слева направо в порядке их посещения и если новые деревья добавляются в лес также слева направо, то все поперечные дуги идут справа налево, что видно на рис. 5.17. Такое взаимное расположение вершин и деревьев выбрано неслучайно, так как это помогает формировать остовный лес. Как можно различить эти четыре типа дуг Легко определить дуги дерева, так как они получаются в процессе обхода графа как дуги, ведущие к тем вершинам, которые ранее не посещались. Предположим, что в процессе обхода орграфа его вершины нумеруются в порядке их посещения. Для этого в листинге процедуры поискав глубину после строки (1) надо добавить следующие строки
dfnumber[v] := count;
count := count + Назовем такую нумерацию глубинной нумерацией ориентированного графа. Всем потомкам вершины
v присваиваются глубинные номера, не меньшие, чем номер, присвоенный вершине
v. Фактически вершина w будет потомком вершины
v тогда и только тогда, когда выполняются неравенства количество потомков вершины
v
1
. Очевидно, что прямые дуги идут от вершин с низкими номерами к вершинам с более высокими номерами, а обратные дуги – от вершин с высокими номерами к вершинам с более низкими номерами. Все поперечные дуги также идут от вершин с высокими номерами к вершинам с более низкими номерами. Чтобы показать это, предположим, что есть дугах у и выполняется неравенство dfnumber(x)≤ dfnumber(y). Отсюда следует, что вершинах пройдена (посещена) раньше вершины у. Каждая вершина, пройденная в промежуток времени от вызова
dfs(x) и до завершения
dfs(y), становится потомком вершины х в глубинном остовном лесу. Если при рассмотрении дуги х → у вершина у еще не посещалась, то эта дуга становится дугой дерева. В противном случае дугах убудет прямой дугой. Таким образом, если для дуги х → у выполняется неравенство, то она не может быть поперечной дугой. Далее будет рассмотрено применение метода поискав глубину для решения различных задач на графах.
1
Для истинных потомков вершины v первое из приведенных неравенств должно быть строгим.
112
5.6. Ориентированные ациклические графы Ориентированный ациклический граф – это орграф, не имеющий циклов. Можно сказать, что ациклический орграф более общая структура, чем дерево, но менее общая по сравнению с обычным ориентированным графом. На рис. 5.18 представлены дерево, ациклический орграф и ориентированный граф с циклом. Рис. 5.18. Три ориентированных графа Ациклические ориентированные графы удобны для представления синтаксических структур арифметических выражений, имеющих повторяющиеся подвыражения. Например, на рис. 5.19 показан ациклический орграф для выражения аса+ ее. Подвыражения аи (ас встречаются в выражении несколько раз, поэтому они представлены вершинами, в которые входят несколько дуг. Рис. 5.19. Ориентированный ациклический граф для арифметического выражения Ациклические орграфы также полезны для представления отношений частичных порядков. Отношением частичного порядка
R, определенным на множестве
S, называется бинарное отношение, для которого выполняются следующие условия
113 1. Ни для какого элемента а из множества S не выполняется aRa свойство антирефлексивности).
2. Для любых ас из S, таких, что аи, выполняется ас свойство транзитивности. Двумя естественными примерами отношений частичного порядка могут служить отношение меньше чем (обозначается знаком «<»), заданное на множестве целых чисел, и отношение строгого включения (
) множеств. Например, пусть
S = {1, 2, 3} и P(S) – множество всех подмножеств множества
S, те. Отношение строгого включения (
) является отношением частичного порядка на множестве
P(S). Очевидно, включение А
Ане выполняется для любого множества А из Р (антирефлексивность), и при выполнении А
В и В
С выполняется АС (транзитивность. Ациклические орграфы можно использовать для графического изображения отношения частичного порядка между какими-либо объектами. Для начала можно представить отношение
R как одноименное множество пар (дуг) таких, что пара (а, b) принадлежит этому множеству только тогда, когда выполняется
aRb. Если отношение R определено на множестве элементов
S, то ориентированный граф G = (S, R) будет ациклическим. И наоборот, пусть
G = (S, R) является ациклическим орграфом, а R
+
– отношение, определенное следующим образом
aR
+
b выполняется тогда и только тогда, когда существует путь (длиной не менее 1) от вершины а к вершине. Тогда R
+
– отношение частичного порядка на
S. (Отношение R
+
является транзитивным замыканием отношения
R.) Рис. 5.20. Ациклический граф, представляющий отношение строгого включения В качестве примера на рис. 5.20 показан ациклический орграф (
P(S),
R), где S = {1, 2, 3}. Здесь R
+
– отношение строгого включения на множестве Проверка ацикличности орграфа Предположим, что есть ориентированный граф
G = (V, Е. Необходимо определить, является ли он ациклическим, те. имеет ли он циклы. Чтобы ответить на этот вопрос, можно использовать метод поискав глубину. Если при обходе орграфа
G методом поискав глубину встретится обратная дуга, то ясно, что граф имеет цикл. С другой стороны, если в орграфе есть цикл, тогда обратная дуга обязательно встретится при обходе этого орграфа методом поискав глубину. Чтобы доказать это, предположим, что орграф имеет цикл (рис. 5.21). Пусть при обходе данного орграфа методом поискав глубину вершина
v имеет наименьшее глубинное число среди всех вершин, составляющих цикл. Рассмотрим дугу
u → v, принадлежащую этому циклу. Поскольку вершина
u входит в цикл, то она должна быть потомком вершины
v в Рис. 5.21. Ациклический орграф, представляющий структуру предшествова- ний глубинном остовном лесу. Поэтому дуга
u → v не может быть поперечной дугой. Так как глубинный номер вершины
u больше глубинного номера вершины
v, то отсюда следует, что эта дуга не может быть дугой дерева и прямой дугой. Следовательно, дуга
u → v является обратной дугой, как показано на рис. 5.21. Топологическая сортировка Большие проекты часто разбиваются на совокупность меньших задач, которые выполняются в определенном порядке и обеспечивают полное завершение целого проекта. Например, план учебного заведения требует определенной последовательности в чтении учебных курсов. Ациклические орграфы могут служить естественной моделью для таких ситуаций. Например, когда чтение учебного курса С предшествует чтению учебного курса
D, создается дуга от курса С к курсу
D. Чтобы проиллюстрировать этот пример приведем ациклический орграф (рис.
5.22), представляющий структуру предше- ствований пяти учебных курсов. Чтение учебного курса С, например, требует предварительного чтения курсов Си С. Рис. 5.22. Ациклический орграф, представляющий структуру предшествований
Топологическая сортировка – это процесс линейного упорядочива- ния вершин ациклического орграфа таким образом, что если существует
115 дуга от вершины
i к вершине j, тов упорядоченном списке вершин орграфа вершина
i предшествует вершине j. Например, для орграфа из рис. 5.22 вершины после топологической сортировки расположатся в следующем порядке С, С, С, С, С. Топологическую сортировку легко выполнить с помощью модифицированной процедуры поискав глубину, если в ней после строки (4) добавить оператор вывода на печать
procedure topsort (v: вершина печать в обратном топологическом порядке вершин, достижимых из вершины v}
w: вершина
begin
mark[v] := visited;
for каждая вершина w из списка L[v] do
if mark[w] = unvisited then
topsort (w);
writeln (v)
end; Когда процедура
topsort заканчивает поиск всех вершин, являющихся потомками вершины
v, печатается сама вершина v. Отсюда следует, что процедура
topsort распечатывает все вершины в обратном топологическом порядке. Эта процедура работает вследствие того, что в ациклическом орграфе нет обратных дуг. Рассмотрим, что происходит, когда поиск в глубину достигает конечной вершины х. Из любой вершины могут исходить только дуги дерева, прямые и поперечные дуги. Но все эти дуги направлены в вершины, которые уже пройдены (посещались до достижения вершины хи поэтому предшествуют вершине х.
M =
999 1998 250250 247748 499499 498498 Прямой выбор С =
M =
499500 2997 499500 7478 499500 252997 Прямой обмен С =
M =
499500 0
499500 749250 499500 1498500
54 Для практических целей полезно иметь некоторые экспериментальные данные об эффективности того или иного алгоритма. В табл. 3.3 показано время работы (в секундах, обсуждавшихся выше методов сортировки, реализованных на персональной ЭВМ
Lilith. Три столбца содержат времена сортировки уже упорядоченного массива, массива со случайными числами и массива, расположенного в обратном порядке. Вначале приводятся цифры для массива, состоящего из 256 элементов, а затем – из 2048 элементов. Таблица 3.3 Время работы различных методов Метод Массив Упорядоченный Случайный В обратном порядке
n = 256 Прямое включение 0.02 0.82 1.64 Двоичное включение 0.12 0.70 1.30 Прямой выбор 0.94 0.96 1.18 Метод пузырька 1.26 2.04 2.80 Метод шейкера 0.02 1.66 2.92 Сортировка Шелла 0.10 0.24 0.28
n = 2048 Прямое включение 0.22 50.74 103.80 Двоичное включение 1.16 37.66 76.06 Прямой выбор 58.18 58.34 73.46 Метод пузырька 80.18 128.84 178.66 Метод шейкера 0.16 104.44 187.36 Сортировка Шелла 0.80 7.08 12.34 Очевидно преимущество сортировки Шелла как улучшенного метода, по сравнению с прямыми методами сортировки. Кроме того, можно отметить следующее
1. Улучшение двоичного включения по сравнению с прямым включением в действительности почти ничего не даст, а в случае упорядоченного массива даже получается отрицательный эффект.
2. Пузырьковая сортировка определенно наихудшая из всех сравниваемых. Ее усовершенствованная версия – шейкерная сортировка – продолжает оставаться плохой по сравнению с прямым включением и прямым выбором (за исключением патологического случая уже упорядоченного массива. Контрольные задания
1. Реализовать в программе два алгоритма из указанных ниже а) сортировка с помощью прямого включения, б) сортировка с помощью двоичного включения,
55 в) сортировка с помощью прямого выбора, г) шейкерная сортировка, д) сортировка Шелла. Сравнить эффективность реализованных алгоритмов по числу перестановок.
2. Запрограммировать три метода прямой сортировки. Сравнить в программе время выполнения каждого из методов.
3. Предположим, что необходимо отсортировать массив, состоящий из нескольких случайных элементов и следующих за ними упорядоченных элементов. Какой из рассмотренных в данной главе методов сортировки наиболее подходит для решения данной задачи
4. Алгоритм называется устойчивым, если он сохраняет исходный порядок элементов с одинаковыми значениями. Какой из алгоритмов, представленных в данной главе, устойчив
5. Рассмотрим алгоритм случайной сортировки, примененный к массиву целых чисел выбирается случайное число i из интервала от 1 дои меняются местами элементы a[0] и a[i], процесс повторяется до тех пор, пока не будут упорядочены все элементы массива. Каково ожидаемое время выполнения массива
6. Написать программу нахождения
k наименьших элементов в массиве длиной
n. Для каких значений k эффективнее сначала выполнить сортировку всего массива, а затем взять
k наименьших элементов, вместо поиска наименьших элементов в неупорядоченном массиве
56
4. СОРТИРОВКА ПОСЛЕДОВАТЕЛЬНОСТЕЙ Алгоритмы сортировки массивов, рассмотренные во третьей главе, предполагают, что объем данных позволяет разместить и произвести их сортировку, используя исключительно оперативную память компьютера. В том случае, если объем сортируемых данных значительно превышает возможности оперативной памяти, приходится задействовать устройства внешней памяти. Однако характеристики доступа к устройствам внешней памяти отличаются от характеристик доступа к оперативной памяти. Это вынуждает использовать отличные структуры и алгоритмы обработки данных. Рассматриваемые в данной главе алгоритмы предназначены для сортировки данных, представляющие собой (последовательный) файл, которые будем называть последовательность. Сортировка данных, организованных в виде файлов, называется внешней сортировкой. Для каждого файла характерно, что в каждый момент непосредственно доступна одна и только одна компонента. Это весьма сильное ограничение, если сравнивать с возможностями, предоставляемыми массивами, и поэтому приходится пользоваться другими методами сортировки.
4.1. Простое слияние Слияние означает объединение двух (или более) последовательностей в одну-единственную упорядоченную последовательность. Объединение происходит при помощи повторяющегося выбора элемента, удовлетворяющего заданному условию, из ряда доступных в данный момент элементов. Слияние намного проще сортировки, и его используют как вспомогательную операцию в более сложных процессах сортировки последовательностей. Наиболее простыми в тоже время основополагающим методом сортировки данных на основе слияния является сортировка простым слиянием. Она выполняется следующим образом
1. Последовательность а разбивается на две половины b и с. Разбиение происходит следующим образом первый элемент последовательности
a записывается в последовательность b, второй элемент последовательности записывается в последовательность c, третий – снова в b, четвертый – в
c и т. д.
2. Обе последовательности
b и с сливаются в a. При этом одиночные элементы последовательностей
b и с сравниваются и сливаются в последовательность в порядке возрастания (убывания, образуя упорядоченные пары.
57 3. Полученная последовательность а вновь разбивается на две – b и c. Данный шаг аналогичен шагу 1, однако разбиение происходит не на единичные элементы, а на упорядоченные пары. То есть первая пара последовательности записывается в последовательность b, вторая – в последовательность, третья пара последовательности a записывается в последовательность, четвертая – в c и т. д.
4. Слияние последовательностей
b и св последовательность a. При этом поочередно сравниваются элементы соответствующих пар последовательностей и си сливаются в последовательность a в порядке возрастания (убывания, образуя упорядоченные четверки.
5. Повторяя предыдущие шаги, разбиваем и сливаем четверки в восьмерки и т. д, каждый раз удваивая длину слитых подпоследовательностей до тех пор, пока не будет упорядочена целиком вся последовательность а. Рассмотрим в качестве примера следующую последовательность
a: 54 35 12 30 16 24 92 19 После разбиения (шаг 1) получаем последовательности
b: 54 12 16 92
c: 35 30 24 19 Слияние одиночных компонент, те. упорядоченных подпоследовательностей длины 1, на шаге 2 даст последовательность
a, состоящую из упорядоченных пар. Для удобочитаемости пары в последовательности ограничены апострофами
a: 35 54 ‘ 12 30 ‘ 16 24 ‘ 19 92 На шаге 3 последовательность
a будет разбита следующим образом
b: 35 54 ‘ 16 24
c: 12 30 ‘ 19 92 Последовательности
b и c также содержат упорядоченные пары. При слиянии этих последовательностей алгоритму доступен только один самый левый) элемент каждой последовательности. Сравнивание элементов последовательностей
b и c происходит только в соответствующих парах. Работа алгоритма на шаге 4 выглядит следующим образом
b: 35 54 ‘ 16 24
c: 12 30 ‘ 19 92 сравниваются элементы 12 итак как 12 < 35, то
a: 12
58 и
b: 35 54 ‘ 16 24
c: 30 ‘ 19 92 далее сравниваются элементы 30 итак как 30 < 35, то
a: 12 30 и
b: 35 54 ‘ 16 24
c: ‘ 19 92 Поскольку сравнивание происходит только в соответствующих парах, а пара последовательности с уже не содержит элементов, то пара последовательности полностью сливается в последовательность a:
a: 12 30 35 54 ‘ последовательности
b и c содержат элементы
b: 16 24
c: 19 92 Теперь начинается сравнение элементов вторых пар последовательностей и c и слияние их в последовательность а Результатом четвертого шага работы алгоритма будет последовательность а, содержащая упорядоченные четверки
a: 12 30 35 54 ‘ 16 19 24 92 Очередное разбиение последовательности а на последовательности b и сдаст Их слияние приводит, наконец, к желаемому результату
a: 12 16 19 24 30 35 54 92 Исходя из описания алгоритма сортировки и представленного выше примера, можно выделить два основных действия над последовательностями это разбиение исходной последовательности на две и их последующее слияние. Такие действия называются фазами фазы разбиения и фазы слияния (рис. 4.1). Наименьший подпроцесс, повторение которого составляет процесс сортировки, называется проходом или этапом. В приведенном примере сортировка происходит затри прохода, каждый из которых состоит из фазы разбиения и фазы слияния. Для выполнения данной сортировки потребовалось три файла или ленты, поэтому она называется трехленточным слиянием.
b
b
b
a
a
a
a
a
c
c
c фаза слияния фаза разбиения проход 1 проход 2 проход n Рис. 4.1. Фазы сортировки и проходы Фазы разбиения фактически не относятся к сортировке, ведь в них элементы не переставляются. В некотором смысле они непродуктивны, хотя и занимают половину всех операций по переписи. Если объединить разделение со слиянием, то от этих переписей можно вообще избавиться. Вместо слияния в одну последовательность результаты слияния будем сразу распределять по двум лентам, которые станут исходными для последующего прохода. В отличие от упомянутой двухфазной сортировки с помощью слияния будем называть такую сортировку однофазной. Она, очевидно, лучше, поскольку необходима только половина операций по переписи, но для этого приходится задействовать четвертую ленту. Анализ сортировки с помощью прямого слияния. Поскольку на каждом проходе длина подпоследовательностей увеличивается вдвое, и сортировка заканчивается, когда длина подпоследовательности станет больше или равной длине исходной последовательности, то всего потребуется) проходов. По определению на каждом проходе все n элементов копируются по одному разу, поэтому общее число перестановок
M = n * log(n). Число сравнений элементов С даже меньше M, так как при копировании остатков подпоследовательностей сравнения не производятся. Поскольку сортировка слиянием производится на внешних запоминающих устройствах, то затраты на операции пересылки на несколько порядков превышают затраты на сравнения. Поэтому детальный анализ числа сравнений особого практического интереса не представляет.
60
4.2. Естественное слияние При сортировке простым слиянием данные разбиваются и сливаются в подпоследовательности длина, которых равна степени двойки (2
k
, где
k ≥
0). Однако данные исходной последовательности могут быть уже частично упорядочены. В этом случае целесообразно просто объединить уже упорядоченные подпоследовательности. Две упорядоченные подпоследовательности длиной
m и n, содержащиеся в двух файлах, дадут на фазе слияния одну подпоследовательность из
m + n элементов. Сортировка, при которой сливаются упорядоченные подпоследовательности, называется естественным слиянием. Упорядоченные подпоследовательности называют сериями. В каждой серии элемент
r
i
не больше, чем
r
i+1
. Таким образом, в сортировке естественным слиянием объединяются серии, а непоследовательности заранее) фиксированной длины. Если сливаются две последовательности, каждая из
n серий, то результирующая также содержит ровно n серий. Следовательно, при каждом проходе общее число серий уменьшается вдвое и общее число пересылок в самом плохом случае равно
n * log(n), а в среднем даже меньше. Ожидаемое число сравнений, однако, значительно больше, поскольку кроме сравнений, необходимых для отбора элементов при слиянии, нужны еще дополнительные сравнения между последовательными элементами каждого файла, чтобы определить конец серии. Рассмотрим естественное слияние на примере двухфазной сортировки стремя лентами. Предположим, что файл
a содержит начальную последовательность элементов.
Файлы b и c – вспомогательные. Каждый проход состоит из фазы распределения серий из
a поочередно в b и c и фазы слияния, объединяющей серии из
b ив. Этот процесс соответствует показанному на рис. 4.1. Пусть исходная последовательность
a содержит следующие данные
a: 15 24 32 ‘ 18 45 ‘ 40 51 ‘ 21 29 31 43 ‘ 42 60 Серии в последовательности
a отделены апострофами. При разбиении исходной последовательности
a в последовательности b и c серии будут распределены следующим образом
b: 15 24 32 ‘ 40 51 ‘ 42 60
c: 18 45 ‘ 21 29 31 43 Как уже отмечалось, длина каждой серии не является заранее заданной, а вычисляется в процессе работы алгоритма. В данном примере в последовательность были записаны серии длиной 3, 2 и 2 элемента. Однако эти значения не запоминаются, а конец каждой серии определятся заново на очередной фазе. Последний элемент первой из записанных серий последовательности меньше первого элемента второй серии последовательности. Поэтому на фазе слияния эти две серии будут рассматриваться, как одна серия состоящая из пяти элементов
b: 15 24 32 40 51 ‘ 42 60 Таким образом, последовательность
a получается из слияния следующих последовательностей
b: 15 24 32 40 51 ‘ 42 60
c: 18 45 ‘ 21 29 31 43. Доступ к данным, хранящимся в файлах
b и c, аналогичен доступу в прямом слиянии. То есть в любой момент времени доступен один и только один элемент каждой последовательности. В демонстрируемом примере считываемые элементы находятся слева. Именно, эти элементы сравниваются, и наименьший из них записывается в файл
a. После естественного слияния последовательностей
b и c получаем
a: 15 18 24 32 40 45 51 ‘ 21 29 31 42 43 60. Наследующем шаге разбиение последовательности
a даст
b: 15 18 24 32 40 45 51
c: 21 29 31 42 43 60. Ив результате слияния этих двух последовательностей получаем отсортированную последовательность
a: 15 18 21 24 29 31 32 40 42 43 45 51 60. Таким образом, для сортировки исходной последовательности естественным слиянием понадобилось два прохода, в то время как для сортировки прямым слиянием понадобилось бы четыре прохода. Алгоритм сортировки естественным слиянием во многом похож на сортировку прямым слиянием. Единственное различие заключается с том, что длина подпоследовательностей в прямом слиянии кратна двойке 1, 2,
4, 8, 16 и т. дав естественном слиянии длина серии вычисляется в процессе считывания элементов последовательности. И именно этим реализация алгоритма естественного слияния оказывается сложнее. Серьезную трудность представляет собой определение конца серии, поскольку для этого необходимо сравнивать значения двух последовательных элементов. Однако природа последовательности такова, что непосредственно доступен только один-единственный элемент. Для того чтобы заглянуть вперед можно организовать своеобразный буфер, введя переменную, куда будет считываться текущий (самый левый) элемент последовательности. Таким образом, можно будет сравнить только что считанный элемент, который теперь хранится в буфере, со следующим за ним элементом последовательности. Если элемент, считанный в буфер, меньше следующего за ним, то элементы принадлежат одной серии. Если же элемент, содержащийся в буфере, оказывается больше последующего элемента, это обозначает конец текущей серии и начало новой. Программный код разбиения исходной последовательности
a на серии и запись их в последовательности
b и c представлен ниже. Имя пере- менной-буфера, в которой сохраняется значение прочитанного элемента последовательности, –
buf. Для файла с исходной последовательностью a используется указатель
f, для последовательностей b и c – указатели file[0] и
file[1], соответственно. Все три указателя имеют тип FILE *.
f = исходный файл a>, "r");
file[0] = файл b>, "w");
file[1] = файл c>, "w");
fscanf(f, "%d", &elem);
// считали один элемент из
// последовательности а
fprintf( file[0], "%d ", elem);
// записали один элемент в
b
buf = elem; // buf – переменная-буфер, содержащая значение
// текущего элемента последовательности
a
j = 0;
//
j служит индексом для записи в последовательности b и
c,
// если
j = 0 запись происходит в последовательность b,
// если
j = 1 запись происходит в последовательность с
while( !feof(f) )
// перепись одного элемента в файл
b или c
{
fscanf(f, "%d", &elem);
if( elem >= buf )
// серия не кончилась, запись в тот же файл
{
fprintf( file[j], "%d ", elem);
buf = elem;
}
else
// серия закончилась, начало записи в другой файл
{
j = 1 – j; // индекс
j теперьуказывает на другой файл
fprintf( file[j], "%d ", elem);
buf = elem;
}
}
63 Если ввести переменную, в которой при слиянии будут подсчитываться серии, то работу алгоритма можно завершить, когда в последовательности будет всего одна серия. Это свидетельствует о том, что исходная последовательность уже отсортирована.
1 2 3 4 5 6 7 8 9 ... 14
4.3. Многопутевая сортировка Затраты на сортировку последовательностей пропорциональны числу выполняемых проходов, так как при каждом проходе копируются все данные. Одним из подходов к повышению эффективности сортировок слиянием является вовлечение в процесс дополнительных каналов обмена данными. Распределение серий на более чем две ленты позволит значительно сократить количество перестановок элементов. Сортировка, при которой используется несколько файлов, называется многопутевым или путевым слиянием. Общее число проходов необходимых для сортировки многопутевым слиянием последовательности из
n элементов равно log
N
(
n). Поскольку при каждом проходе выполняется n операций копирования, тов худшем случае общее число перестановок будет равным
M = n * log
N
(
n). При сортировке путевым слиянием используются 2 * N последовательностей. Предполагается, что число входных файлов равно числу выходных. На каждом проходе серии входных последовательностей сливаются, объединяясь, в выходные последовательности. После того, как данные во входных последовательностях заканчиваются, последовательности меняются ролями – входные последовательности становятся выходными и наоборот.
Многопутевое слияние основывается на естественном слиянии с од- ной-единственной фазой. Также как ив случае обычного двухфазного трехленточного естественного слияния возможно слияние двух следующих одна за другой серий в одну, если последний элемент какой-либо серии меньше первого элемента серии, следующей за ней. Рассмотрим в качестве примера сортировку путевым слиянием следующей последовательности
f: 57 24 88 13 19 17 96 37 42 15 21 35 23 10 53 49 33 58 16 72. Для путевого слияния будут использованы 6 файлов 3 входных и 3 выходных. Первым этапом сортировки многопутевым слиянием является разбиение исходной последовательности. Все данные разбиваются на серии и записываются в файлы
f
1
,
f
2
,
f
3
. Серии в этих файлах отделены апострофами ‘ 49
f
3
: 13 19 ‘ 15 21 35 ‘ 33 58 Поскольку в основе многопутевой сортировки лежит естественное слияние, то длина каждой серии заранее неизвестна и определяется непосредственно в процессе чтения данных из файла. Указанные выше файлы
f
1
,
f
2
,
f
3
послужат на первом проходе входными файлами, те. из них будут считываться серии и сливаться в выходные файлы
f
4
,
f
5
,
f
6
. Вначале работы алгоритма сортировки выходные файлы пусты. На первой итерации первого прохода первые серии входных файлов сливаются в одну серию, записываемую в выходной файл
f
4
:
f
1
: 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57 88
f
2
: 37 42 ‘ 10 53 ‘ 49
f
5
: .
f
3
: 15 21 35 ‘ 33 58
f
6
: Результатом второй итерации первого прохода будут слияние вторых серий входных файлов в выходной файл
f
5
:
f
1
: 23 ‘ 16 72
f
4
: 13 19 24 57 88
f
2
: 10 53 ‘ 49
f
5
: 15 17 21 35 37 42 96.
f
3
: 33 58
f
6
: На третей итерации первого прохода будет заполнен выходной файл
f
6
:
f
1
: 16 72
f
4
: 13 19 24 57 88
f
2
: 49
f
5
: 15 17 21 35 37 42 96.
f
3
:
f
6
: 10 23 33 53 58 Первый проход завершается на четвертой итерации. Входной файл
f
3 пуст, поэтому в выходной файл
f
4
сливаются серии, находящиеся только в файлах
f
1
и f
2
, все данные входных файлов заканчиваются
f
1
:
f
4
: 13 19 24 57 88 ‘ 16 49 72.
f
2
:
f
5
: 15 17 21 35 37 42 96
f
3
:
f
6
: 10 23 33 53 58 Если бы во входных файлах еще содержались данные, то серии сливались бы в файл
f
5
, затем f
6
, снова
f
4
,
f
5
,
f
6
и т. д, пока не закончатся все данные во всех входных файлах. После того, как входные файлы становятся пустыми, выполняется второй проход. На втором проходе входные файлы
f
1
,
f
2
,
f
3
становятся выходными и уже в них сливаются серии из файлов
f
4
,
f
5
,
f
6
, которые на втором проходе становятся входными
f
4
: 13 19 24 57 88 ‘ 16 49 72
f
1
:
f
5
: 15 17 21 35 37 42 96
f
2
:
f
6
: 10 23 33 53 58
f
3
: Результат первого слияния на втором проходе алгоритма следующий 58 88 96
f
5
:
f
2
:
f
6
:
f
3
: Второй проход заканчивается на втором слиянии из единственного содержащего данные файла
f
4 в выходной файл
f
2
f
4
:
f
1
: 10 13 15 17 19 21 23 24 33 35 37 42 53 57 58 88 96
f
5
:
f
2
: 16 49 72
f
6
:
f
3
: На третьем проходе входные и выходные файлы опять меняются ролями, теперь
f
1
,
f
2
,
f
3
– входные файлы, а
f
4
,
f
5
,
f
6
– выходные. Алгоритм путевого слияния заканчивает свою работу на третьем проходе. На этом проходе выполняется только одна итерация, на которой серии из файлов
f
1
и f
2
сливаются в файл
f
4
:
f
1
:
f
4
: 10 13 15 16 17 19 21 23 24 33 35 37 42 49 53 57 58 72 88 96
f
2
:
f
5
:
f
3
:
f
6
: Полученную последовательность можно для удобства записать вис- ходный файл
f. Рассмотренный пример показывает, что алгоритм многопутевого слияния работает эффективнее, чем рассмотренные ранее алгоритмы прямого и естественного слияния. Так, при путевом слиянии для сортировки указанной последовательности потребовалось только три прохода, в то время как для естественного и прямого слияния потребовалось бы четыре и пять проходов, соответственно. К тому же последние два алгоритма требуют двойного копирования, поскольку состоят из двух фаз разбиения и слияния.
В нашем примере используются шесть входных и выходных файлов, но для более эффективной работы их число может быть увеличено. Это требуется для работы с большими последовательностями, содержащими большое количество элементов. В этом случае будет затрачено значительно меньше времени на распределение и последующее слияние. Теперь рассмотрим несколько вопросов связанных с реализацией алгоритма многопутевого слияния. Поскольку при сортировке последовательностей многопутевым слиянием используется достаточно большое количество файлов, то для ссылки на файлы целесообразно использовать не отдельные переменные, а массив файлов. Если объявить имена и указатели на используемые файлы следующим образом
FILE *file[6];
char filename[6][10] = {{"f1.txt"},
66
{"
f2.txt"},
{"
f3.txt"},
{"
f4.txt"},
{"
f5.txt"},
{"
f6.txt"}}; то можно легко открыть на чтение и запись соответствующие файлы
for(i = 0; i < 3; i++)
// первые три файла открываются на чтение
file[i] = fopen(filename[i],"r");
for(i = 3; i < 6; i++) // остальные три файла открываются на запись
file[i] = fopen(filename[i],"w"); Такой подход обладаем еще одним серьезным преимуществом. Теперь для доступа к какому-либо файлу можно использовать его индекс, что крайне полезно при переключении группы входных и выходных последовательностей в конце каждого прохода. Для решения задачи переключения лент можно рекомендовать технику карты индексов лент. Вместо прямой адресации ленты с помощью индекса
i она адресуется через карту t, которая объявляется как
int t[2 * N]; // 2 * N – общее число входных и выходных файлов. Вначале работы алгоритма
t
i
=
i для всех i. Переключение же входных и выходных файлов представляет собой обмен местами компонент
t
i
и
t
i + N
для всех
i = 1, …, N. В этом случае всегда можем считать, что f
t1
, …,
f
tN
– входные последовательности, a
f
t(N + 1)
, …,
f
t(2*N)
– выходные. Для описанного выше примера путевой (
N = 3) сортировки карта индексов лент определяется как
int t[6];
for(i = 0; i < 2 * N; i++)
t[i] = i; Начальные значения элементов данной карты индексов лент представлены в табл. 4.1. Здесь также указаны файлы, на которые ссылается каждый элемент карты индексов лент. Таблица 4.1 Карта индексов лент
t
i
i Ссылка на файл
t[1] 1
f
1
t[2] 2
f
2
t[3] 3
f
3
t[4] 4
f
4
t[5] 5
f
5
t[6] 6
f
6
67 При переключении входных и выходных файлов меняются местами компоненты
t
i
и
t
i + 3
, для всех
i = 1, …, 3. Результат этого процесса показан в табл. 4.2. Таблица 4.2 Карта индексов лент после переключения входных и выходных файлов
t
i
i Ссылка на файл
t[1] 4
f
4
t[2] 5
f
5
t[3] 6
f
6
t[4] 1
f
1
t[5] 2
f
2
t[6] 3 Таким образом, входные последовательности всегда находятся вверх- ней половине карты индексов лент и к ним всегда можно получить доступ через
t[1], t[2] и t[3]. Выходные последовательности находятся в нижней половине, доступ к ним всегда осуществляется через
t[4], t[5] и t[6]. Открыть входные файлы на чтение, а выходные на запись при помощи карты индексов лент можно следующим образом
for(i = 0; i < 3; i++) // открыть входные файлы на чтение
file[t[i]] = fopen (filename[t[i]],"r");
for(i = 3; i < 6; i++) // открыть выходные файлы на запись
file[t[i]] = fopen (filename[t[i]],"w"); Закрыть файлы при помощи карты индексов лент можно так
for(i = 0; i < 6; i++) // закрыть все файлы
fclose(file[t[i]]); Преимущество использования карты индексов лент заключается том, что приведенный код остается неизменным независимо оттого, являются входными файлами
f
1
,
f
2
,
f
3
или же
f
4
,
f
5
,
f
6
. Главной задачей становится указание, на какой файл должен ссылаться каждый элемент карты индекса лент При новом проходе входные файлы становятся выходными, те. их индексы перемещаются во вторую половину карты индексов лент, выходные же файлы становятся входными – их индексы перемещаются в первую половину карты индексов лент. Таким образом происходит переиндекса- ция файлов. Затем входные файлы открываются на чтение, выходные – на запись, и выполняется поочередное слияние серий из входных в файлов в выходные. В завершении каждого прохода все файлы закрываются.
68 При слиянии серий необходимо точно идентифицировать текущие входные последовательности. Может оказаться, что число входных файлов будет меньше, чем
N. Данная ситуация характерна для последних проходов. Так, в примере путевого слияния вначале третьего прохода число входных файлов равно двум. Поэтому следует ввести некоторую переменную, скажем
k1, задающую фактическое число работающих входов. Инициализация может выглядеть следующим образом
if (L < N) k1 = L;
else k1 = N; где
L – число серий, полученных при последнем слиянии. Возвращаясь к примеру путевого слияния, можно сказать, что вначале первого прохода
k1 = 3, в то время, как вначале второго слияния на втором проходе, где входной файл
f
4
содержит элементы 16, 49, 72, а остальные два входных файла
f
5
и f
6
пусты,
k1 = 1. Понятно, что по исчерпании какого-либо входа
kl должен уменьшаться. Слияние серий входных файлов включает в себя повторяющийся выбор наименьшего из элементов соответствующих серий и отсылку его в выходной файл. Этот процесс усложняется необходимостью определять конец каждой серии. Конец серии достигается в двух случаях очередной элемент меньше текущего элемента достигнут конец входного файла. В последнем случае вход исключается из работы и
kl уменьшается. В первом же серия закрывается, элементы соответствующей последовательности в выборе уже не участвуют, но это продолжается лишь до того, как будут считаны все данные соответствующих серий из других входных последовательностей. Отсюда следует, что нужна еще одна переменная, скажем
k2, указывающая число входов, действительно используемых при выборе очередного элемента. Вначале ее значение устанавливается равными уменьшается всякий раз, когда серия заканчивается. Опять же возвращаясь к ранее рассмотренному примеру, вначале первого прохода имеем
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: 13 19 ‘ 15 21 35 ‘ 33 58 Входными файлами являются
f
1
,
f
2
,
f
3
, переменные
k1 и k2 равны трем. При слиянии первых серий этих трех последовательностей в выходной файл
f
4
значение
k2 изменяется следующим образом
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: 19 ‘ 15 21 35 ‘ 33 58
k2 = 3
69
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2 (закончилась серия в
f
3
)
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 1 (закончилась серия в
f
1
)
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57 88
f
2
: ‘ 37 42 ‘ 10 53 ‘ 49
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 0 (закончилась серия в
f
2
) Переменная
k2 приняла значение ноль, значит, данные первых серий закончились. Переходим к слиянию вторых серий входных последовательностей. Выполняется проверка на наличие пустых входных файлов. Поскольку все три входных файла содержат данные, то
k1 = 3, и переменная, отражающая число активныхвходов, инициализируется значением
k1, те. Начинается слияние вторых серий в выходной файл Можно сказать, что переменная
k1 носит более глобальный характер, поскольку показывает, сколько всего входных файлов находятся в работе. Переменная
k2 отвечает за количество активных входных файлов, те. таких файлов, чьи серии участвуют в слиянии в данный момент. Значение переменной
k2 не может превышать значения k1. К сожалению, недостаточно просто ввести переменную
k2. Необходимо знать не только число активных последовательностей, но и какие, именно, из них используются. Очевидное решение – использовать массив булевых переменных, определяющих доступность последовательностей. Однако существует и другой метод, при котором процедура выбора становится более эффективной, а это, в конечном счете, наиболее часто повторяющаяся часть алгоритма. Вместо булевого массива вводим вторую карту лент
ta, где ta
1
, …,
ta
k2
– индексы доступных входов.
Эта карта будет использоваться вместо карты
t. Размер карты ta в два раза меньше размера карты t, так как она используется для индексации только входных файлов. Вначале каждого прохода карта индексов активных лент
ta инициализируется следующим образом
for(i = 0; i < k1; i++)
ta[i] = t[i];
70 Слияние серий из входных файлов нас помощью карты индексов активных лент
ta можно записать следующим образом
k2 = k1;
do
{ выбор минимального элемента,
ta[mx] – индекс файла, в котором находится минимальный элемент перемещение минимального элемента изв если файл
file[ta[mx]] пуст – исключить входной файл если в файле
file[ta[mx]] закончилась серия – закрыть серию
}
while(k2 != 0); Операция исключить файл»
предполагает уменьшение kl и k2, а также переупорядочение индексов в карте
ta. Оператор закрыть серию»
уменьшает только
k2 и переупорядочивает соответствующим образом ta. Рассмотрим переупорядочение индексов в карте
ta на приведенном ранее примере путевого слияния. На первом проходе входными файлами являются
f
1
,
f
2
,
f
3
, первое слияние серий происходит в файл
f
4
. Начальное распределение серий по входным файлам выглядит следующим образом
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49.
f
3
: 13 19 ‘ 15 21 35 ‘ 33 58 Помимо значений элементов, содержащихся во входных и выходных файлах, приведем значение переменной
k2 и карты индексов лент ta.
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13
ta
1
= 1
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 2
f
3
: 19 ‘ 15 21 35 ‘ 33 58
1 2 3 4 5 6 7 8 9 10 ... 14
k2 = 3
ta
3
= Видно, что первый индекс карты
ta указывает на первый файл, второй индекс – на второй файл, третий индекс – на третий. Так как
k2 = 3, то при слиянии используются все три индекса карты
ta, а, следовательно, и все три входных файла, на которые указывают эти индексы.
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19
ta
1
= 1
f
2
: 24 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 2
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2
ta
3
= 3 (не исп) Серия в файле
f
3
закончилась, поэтому значение переменной
k2 уменьшается на единицу. Это, в свою очередь, означает, что будут использоваться только первые два индекса карты
ta: ta
1
и
ta
2
. Последний, третий, индекс
ta
3
не будет использоваться до тех пор, пока не начнется слияние вторых серий входных файлов. Индекс
ta
3
, который больше не принимается во внимание, должен указывать на тот файл, в котором только что закончилась серия. В данном случае это файл
f
3
, поэтому
ta
3 должен иметь значение 3. (Конечно, они раньше был равен трем, но важным здесь является то, что индекс
ta
3
, который больше не рассматривается, указывает на третий входной файл. Очередное слияние серий приведет к следующему расположению элементов в файлах
f
1
: 57 ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24
ta
1
= 1
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 2
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 2
ta
3
= 3 (не исп) Значение
k2 не изменилось, так как серии в файлах f
1
и
f
2
не закончились. Поскольку при слиянии использовались только индексы
ta
1
и
ta
2
, то файл
f
3
, на который указывает индекс в слиянии не участвовал. При очередном слиянии получаем
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57
ta
1
= 2
f
2
: 88 ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 1 (не исп)
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 1
ta
3
= 3 (не исп) Теперь закончилась серия в файле
f
1
. Значение переменной
k2 уменьшается на единицу. Это значит, что индекс
ta
2
больше не рассматривается. Данный индекс должен получить значение 1, те. указывать на файл с закончившейся серией
f
1
(
ta
2
= 1). Однако нельзя терять его бывшее значение – 2. Это значение должно быть присвоено тому индексу, который ранее указывал на файл
f
1
. Этим индексом является
ta
1 и поэтому
ta
1
= 2. То есть теперь индекс
ta
1 указывает на файл
f
2
, единственный файл, в котором еще осталась незаконченная серия. Следующее слияние приведет к тому, что все элементы первых серий входных файлов закончатся
f
1
: ‘ 17 96 ‘ 23 ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 2 (не исп)
f
2
: ‘ 37 42 ‘ 10 53 ‘ 49
ta
2
= 1 (не исп)
f
3
: ‘ 15 21 35 ‘ 33 58
k2 = 0
ta
3
= 3 (не исп) Значение переменной
k2 станет равным нулю, те. индексы карты ta больше не используются. Так заканчивается слияние первых серий входных последовательностей. Можно лишь добавить, что значение переменной все это время оставалось равным трем, так как не был достигнут конец ни одного из трех входных файлов. Наследующем цикле выполняется присвоение
k2 = k1, те. Это значит, что все индексы карты ta снова используются при слиянии. Значения
ta
i
при этом сохраняются, те. индекс
ta
1 указывает на файл
f
2
,
ta
2 указывает на
f
1
, а
ta
3 на файл
f
3
. Порядок входных файлов в карте индексов не имеет значения. Существенным является лишь то, что все индексы карты индексов активных лент
ta указывают на все существующие входные последовательности. Как уже отмечалось, значение переменной
k1 уменьшается на единицу при достижении конца одного из файлов. Увидеть это можно на
72 третьей итерацией первого прохода, где слияние происходит в выходной файл
f
6
f
1
: 23 ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 1
f
2
: 10 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 33 58
k1 = 3
k2 = 3 f
6
:
ta
3
= 3
f
1
: 23 ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 1
f
2
: 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 33 58
k1 = 3
k2 = 3
f
6
: 10
ta
3
= 3
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3
f
2
: 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 33 58
k1 = 3
k2 = 2
f
6
: 10 23
ta
3
= 1 (не исп)
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3
f
2
: 53 ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2
f
3
: 58
k1 = 3
k2 = 2
f
6
: 10 23 33
ta
3
= 1 (не исп)
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3
f
2
: ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2 (не исп)
f
3
: 58
k1 = 3
k2 = 1
f
6
: 10 23 33 53
ta
3
= 1 (не исп)
f
1
: ‘ 16 72
f
4
: 13 19 24 57 88
ta
1
= 3 (не исп)
f
2
: ‘ 49
f
5
: 15 17 21 35 37 42
ta
2
= 2 (не исп)
f
3
:
k1 = 2
k2 = 0
f
6
: 10 23 33 53 58
ta
3
= 1 (не исп) Наследующей итерации произойдет слияние входных файлов
f
1
ив файл
f
4
, и первый проход завершится. При новом проходе входные ивы- ходные файлы должны поменяться ролями
for(i = 0; i < N; i++)
{
temp = t[i]; t[i] = t[i + N]; t[i + N] = temp; } Проходы выполняются до тех пор, пока не останется последовательность, состоящая из одной-единственной серии. Резюмируя все вышесказанное, можно дать общий набросок программы, реализующий алгоритм многопутевого слияния инициализация индексов карты лент
t
i
, для всех
i = 1, …, 2 * N; распределение серий исходной последовательности во входные файлы
t[1],
…,
t[N], при этом подсчитывается общее количество серий
L; пока
L > 1
{ если
L < N, тогда k1 = L, иначе k1 = N;
73 инициализация индексов карты активных входов
ta
i
=
t
i
, для
i = 1, …, N;
j = N + 1, где j – индекс текущего выходного файла пока
k1 > 0
{
L = L +1;
k2 = k1; пока
k2 > 0
{ поиск минимального элемента в рассматриваемых сериях, ta[mx] – индекс файла, в котором содержится мин. элемент запись минимального элемента изв если конец файла
ta[mx], то k1 = k1 – 1, k2 = k2 – 1, переупорядочивание индексов карты
ta; иначе если серия в
ta[mx] закончилась, то k2 = k2 – 1, переупорядочивание индексов карты
ta;
} смена выходного файла если
j < 2 * N, тогда j = j + 1, иначе j =
N + 1;
} входные и выходные файлы меняются ролями
t
i
меняется с
t
i + N
, для
i = 1, …, N;
}
4.4. Многофазная сортировка В основе сортировки последовательности многофазным слиянием лежит распределение начальных серий в соответствии с числами Фибоначчи. Поэтому, перед тем, как перейти непосредственно к алгоритму многофазной сортировки, рассмотрим числа Фибоначчи. Числа Фибоначчи В 1202 году итальянский математик Леонардо Фибоначчи поставил в своей книге «Liber abacci» следующую задачу Человек посадил пару кроликов в загон, окруженный со всех сторон стеной. Сколько пар кроликов будет через
n месяцев, если известно, что через месяц каждая пара кроликов производит на свет еще одну пару, которая в свою очередь становится способной производить потомство со второго месяца после своего рождения В этой же книге Фибоначчи приводит решение этой задачи количество пар кроликов будет увеличиваться каждый месяц следующим образом
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
74 Иными словами, число пар кроликов создает ряд, каждый член которого равен сумме двух предыдущих. Этот ряд известен как ряд Фибоначчи, асами числа – как числа Фибоначчи.
f
1
f
2
f
3 13(1) 8(1)
5(1) 0 8(2)
0 5(3) 3(2)
3(5) 2(3) 0 1(5) 0 2(8)
0 1(13) 1(8)
1(21) 0 0 Рис. 4.2. Многофазная сортировка стремя последовательностями На рис. 4.2 показано распределение серий при многофазной сортировке слиянием стремя последовательностями, две из которых являются входными, одна – выходной. На каждом проходе элементы из двух входных последовательностей сливаются в третью. Как только одна из входных последовательностей исчерпывается, она становится выходной. Числа, указанные на рис. 4.2, показывают количество серий в последовательности и длину серий. Слияние начинается с двух последовательностей
f
1
и
f
2
, организованных в виде серий длины 1. Серии из
f
1
и
f
2 объединяются, образуя серии длины 2, и записываются в третью последовательность
f
3
. Слияние происходит до полного опустошения последовательности
f
2
(как видно из рис.
4.2 в последовательности
f
2
серий меньше, чем в
f
1
). Затем объединяем оставшиеся серии длины 1 из
f
1
с таким же количеством серий длины 2 из В результате получаются серии длины 3, которые помещаются в файл Затем объединяются серии длины 2 из
f
3
с сериями длины 3 из
f
2
. Эти серии длины 5 помещаются в последовательность
f
1
, которая была исчерпана вовремя предыдущего прохода. Процесс продолжается до тех пор, пока вне окажется отсортированная последовательность. Оказывается, чтобы сохранить нужный ход процесса сортировки, начальное количество серий в
f
1
и
f
2
должно представлять собой два последовательных числа Фибоначчи. Так на рис. 4.2 показан процесс сортировки серии, распределенных следующим образом 13 серий в последовательности и 8 серий – в В табл. 4.3 приведено количество серий во входных файлах
a
1
(L)
и
a
2
(L) и необходимое для их слияния число проходов
L. Понятие прохода при многофазной сортировке менее жесткое, чем в рассмотренных ранее алгоритмах внешней сортировки, и представляет собой слияние серий из входных файлов в выходной, пока один из входных файлов не станет пустым. Как только опустошается один из входных файлов, проход считается завершенным, несмотря на то, что в других входных файлах еще остались серии, не участвовавшие в слиянии. Опустошившийся входной файл на новом проходе становится выходным, все остальные файлы – входными. Таблица 4.3 Идеальное распределение серий по двум последовательностям
L
a
1
(L)
a
2
(L)
Sum a
i
(L)
0 1 0 1 1 1 1 2 2 2 1 3 3 3 2 5 4 5 3 8 5 8 5 13 6 13 8 21 В табл. 4.3 также приведена сумма серий в обеих входных последовательностях. Если забыть об условии, что количество серий, хранящихся в последовательных файлах, заранее неизвестно, то можно сказать, что идеальным для многофазной сортировки будет количество серий равное одному из чисел, приведенных в четвертом столбце табл. 4.3. Здесь же приводится и идеальное распределение серий по входным последовательностям. Так, в рассмотренном примере, исходная последовательность содержит серию, что является идеальным числом серий для алгоритма многофазного слияния. Во входные последовательности будут распределены соответственно 13 и 8 серий. Как видно из табл. 4.3, для сортировки данной последовательности потребуется шесть проходов. Если бы исходная последовательность содержала 8 серий, то 5 из них были бы распределены в первую входную последовательность, 3 – во вторую. Исходная последовательность была бы отсортирована за четыре прохода. Из табл. 4.3 при
L > 0 можно вывести следующие соотношения
a
2
(L + 1)
=
a
1
(L)
,
a
1
(L + 1)
=
a
1
(L)
+
a
2
(L)
,
76 и
a
1
(0)
= 1,
a
2
(0)
= 0. Принимая
f
i
+ 1
=
a
1
(i)
, получаем для
i > 0:
f
i + 1
=
f
i
+
f
i – 1
,
f
1
= 1,
f
0
= 0. Эти рекуррентные отношения задают числа Фибоначчи
f = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... Эти числа Фибоначчи называются числами Фибоначчи первого порядка. Существуют числа Фибоначчи и более высоких порядков. В общем виде числа Фибоначчи порядка
p определяются следующим образом
f
(p)
i + 1
=
f
(p)
i
+ f
(p)
i – 1
+ … + f
(p)
i – p
для
i ≥ p,
f
(p)
p
= 1,
f
(p)
i
= 0 для 0 ≤
i < p. Рассмотрим второй пример, где в многофазном слиянии участвуют шесть последовательностей, одна из которых – выходная. Данный пример показан на рис. 4.3. В данном примере начальное распределение серий следующее в находится 16 серий, в
f
2
– 15, в
f
3
– 14, в
f
4
– 12 серий и 8 серий в
f
5
. Напер- вом проходе сливаются и помещаются в
f
6 8 серий. Обратите внимание, что длина этих восьми серий равна пяти, поскольку в слиянии участвовали серии длины 1, хранящиеся в пяти входных файлах. Процесс слияния серий аналогичен слиянию стремя последовательностями. Заканчивается многофазное слияние с шестью последовательностями через пять проходов, когда
f
2
содержит все отсортированное множество элементов.
f
1
f
2
f
3
f
4
f
5
f
6 16(1) 15(1) 14(1) 12(1) 8(1)
8(1)
7(1)
6(1)
4(1)
0 8(5)
4(1)
3(1)
2(1)
0 4(9) 4(5)
2(1)
1(1)
0 2(17) 2(9) 2(5)
1(1)
0 1(33)
1(17) 1(9) 1(5)
0 1(65)
0 0
0 0 Рис. 4.3. Многофазная сортировка с шестью последовательностями В примере многофазного слияния с шестью последовательностями выполнялась сортировка 65 элементов (или серий длины 1). Такое количество было выбрано преднамеренно, поскольку идеально подходит для многофазной сортировки с шестью последовательностями, в которой используются числа Фибоначчи четвертого порядка. То есть порядок используемых чисел Фибоначчи зависит от количества последовательностей при многофазной сортировки с
N последовательностями используются числа Фибоначчи
N – 2 порядка. В табл. 4.4 приведено распределение серий по пяти входным последовательностям, соответствующих приведенному на рис. 4.3 примеру многофазной сортировки с шестью последовательностями. Таблица 4.4 Идеальное распределение серий по пяти последовательностям
L
a
1
(L)
a
2
(L)
a
3
(L)
a
4
(L)
a
5
(L)
Sum
a
i
(L)
0 1 0 0 0 0 1
1 1 1 1 1 1 5
2 2 2 2 2 1 9
3 4 4 4 3 2 17 4 8 8 7 6 4 33 5 16 15 14 12 8 65 Исходя из табл. 4.4, можно вывести правила образования числа серий на каждом проходе
a
5
(L + 1)
=
a
1
(L)
,
a
4
(L + 1)
=
a
1
(L)
+
a
5
(L)
=
a
1
(L)
+
a
1
(L – 1)
,
a
3
(L + 1)
=
a
1
(L)
+
a
4
(L)
=
a
1
(L)
+
a
1
(L – 1)
+
a
1
(L – 2)
,
a
2
(L + 1)
=
a
1
(L)
+
a
3
(L)
=
a
1
(L)
+
a
1
(L – 1)
+
a
1
(L – 2)
+
a
1
(L – 3)
,
a
1
(L + 1)
=
a
1
(L)
+
a
2
(L)
=
a
1
(L)
+
a
1
(L – 1)
+
a
1
(L – 2)
+
a
1
(L – 3)
+
a
1
(L – 4)
, Подставляя
f
i
вместо
a
1
(i)
, получаем
f
i + 1
=
f
i
+
f
i – 1
+
f
i – 2
+
f
i – 3
+
f
i – 4
, для
i ≥ 4,
f
4
= 1,
f
i
= 0, для
i < 4, что полностью соответствует приведенным выше правилам, определяющим числа Фибоначчи порядка
p. Таким образом, любое из чисел Фибоначчи четвертого порядка равно сумме пяти предшествующих ему чисел. Ряд Фибоначчи четвертого порядка выглядит следующим образом
f
(4)
= 0, 0, 0, 0, 1, 1, 2, 4, 8, 16, ... Эти числа можно увидеть в табл. 4.4 в столбце, соответствующему
a
1
(L)
78 В завершение обсуждения чисел Фибоначчи приведем табл. 4.5, в которой содержится количество серий, идеально подходящих для многофазной сортировки с
N последовательностями, значение L соответствует числу проходов, необходимых для сортировки всех элементов исходной последовательности. Таблица 4.5 Количество серий, допускающее идеальное распределение
L N
3 4 5 6 7 8 1 2 3 4 5 6 7 2 3 5 7 9 11 13 3 5 9 13 17 21 25 4 8 17 25 33 41 49 5 13 31 49 65 81 97 6 21 57 94 129 161 193 7 34 105 181 253 321 385 8 55 193 349 497 636 769 9 89 355 673 977 1261 1531 10 144 653 1297 1921 2501 3049 11 233 1201 2500 3777 4961 6073 12 377 2209 4819 7425 9841 12097 13 610 4063 9289 14597 19521 24097 14 987 7473 17905 28697 38721 48001 Элементами табл. 4.5 являются суммы серий
n последовательностей на определенном проходе. Так значения
Sum a
i
(L)
из табл. 4.3 можно найти в табл. 4.5 с индексом столбца
N = 3 (в многофазной сортировке используются три последовательности. Значения
Sum a
i
(L)
из табл. 4.4 находятся в табл. 4.5 с индексом столбца
N = 6 (используются шесть последовательностей. Начальное распределение серий для алгоритма многофазной сортировки В рассмотренных примерах слияния предполагалось, что количество серий в исходной последовательности имеет значение, соответствующее одному из чисел Фибоначчи, что позволит произвести начальное распределение серий идеальным образом. На практике же это условие выполняется редко. В этом случае предлагается ввести гипотетические пустые серии, которые в сумме с реально существующими сериями дадут число, подходящее для идеального распределения серий. Новые введенные серии называются пустыми сериями. Пустые серии необходимо распределять по входным последовательностям как можно более равномерно, поскольку эффективность алгоритма
79 многофазной сортировки зависит от слияния реальных серий. Если серия последовательности пустая, тов слиянии она не участвует, и слияние происходит не из
N – 1 входных последовательностей, а из меньшего их числа. А при многофазной сортировке должно использоваться как можно большее число последовательностей. Если во всех
N – 1 последовательностях содержатся пустые серии, то никакого реального слияния не происходит, а выходную последовательность записывается пустая серия. Рассмотрим задачу распределения серий исходной последовательности по
N – 1 последовательностям. Поскольку мы имеем дело с внешней сортировкой, то число серий заранее неизвестно. В процессе распределения вычисляются числа Фибоначчи порядка
N – 2, определяющие желательное число серий в каждой из входных последовательностей. Предположим, что общее число последовательностей
N = 6, из которых при сортировке пять будут входными. Следовательно, необходимо распределить серии по этим пяти последовательностям. Так как при распределении серии записываются в пять последовательностей, воспользуемся табл. 4.4. Сначала серии считываются из исходной последовательности и распределяются, как указано в строке с индексом, если в исходной последовательности еще остались серии, то переходим ко второй строке (2, 2, 2, 2, 1), если серии все еще остались, то переходим к третьей строке (4, 4, 4, 3, 2) и т. д. Будем называть индекс строки уровнем Очевидно, чем больше число серий, тем выше уровень чисел Фибоначчи, который в данном случае равен количеству проходов, необходимых для сортировки исходной последовательности. Проиллюстрируем распределение серий на примере с пятью последовательностями. Для оптимального распределения серий воспользуемся алгоритмом горизонтального распределения. Сначала записываем в последовательности по одной серии (рис. 4.4). Числа на рис. 4.4 означают порядок серий, те. первая серия исходной последовательности записывается в
f
1
, вторая серия – в
f
2
и т. д.
1 2 3 4 5 Рис. 4.4. Горизонтальное распределение серий, L = 1 Если исходная последовательность еще содержит серии, то переходим на второй уровень, где число серий в последовательностях
f
1
,
f
2
,
f
3
,
f
4
,
f
5
. должно быть равно, соответственно, 2, 2, 2, 2, 1. На рис. 4.5 показаны серии последовательностей, записанные на первом уровне, а также ячейки, в которые будут теперь записываться серии из исходной последовательности.
80 Фактически пустые ячейки представляют собой пустые серии, которые замещаются реальными сериями при считывании их из исходной последовательности Рис. 4.5. Горизонтальное распределение серий, L = 2 Введем переменные
a
i
и
d
i
, отражающие идеальное число серий и число пустых серий для последовательности
i. Вначале работы алгоритма горизонтального распределения эти переменные инициализируются следующим образом
a
i
= 1,
d
i
= 1, для
i = 1, …, N – 1. Эти значения соответствуют рис. 4.4. Значения
a
i
приведены в табл. 4.4 и изменяются в соответствии с уровнем
L. Значения d
i
определяются как разность
a
i
текущего и предыдущего уровней
d
i
=
a
i
(L)
–
a
i
(L – 1)
. На рис. 4.6 приведены значения переменных и
d
i
при переходе на второй уровень.
d
i
1 1 1 1 0
a
i
2 2 2 2 1 1 2 3 4 5 Рис. 4.6. Горизонтальное распределение серий при переходе на второй уровень По мере того, как считываются реальные серии из исходной последовательности, замещая пустые серии входных последовательностей, меняются значения
d
i
(рис. 4.7, 4.8). Переход на третий уровень (
L = 3) приведет к изменению значений переменных a
i
и
d
i
, показанному на рис. 4.9. Поскольку пустые серии должны быть распределены по последовательностям равномерно, то десятая серия исходной последовательности будет записана не в
f
5
(что привело быка встанет равным 1). Запись десятой серии показана на рис. 4.11. Аналогично, одиннадцатая и двенадцатая серии будут записаны в
f
2 и
f
3
. После этого запись серий будет происходить поочередно, начиная с по
f
5
(рис. 4.11, 4.12).
81
d
i
0 1 1 1 0
a
i
2 2 2 2 1 6
1 2 3 4 5 Рис. 4.7. Горизонтальное распределение при считывании шестой серии
d
i
0 0 0 0 0
a
i
2 2 2 2 1 6 7 8 9 1 2 3 4 5 Рис. 4.8. Горизонтальное распределение при считывании девятой серии
d
i
2 2 2 1 1
a
i
4 4 4 3 2 6 7 8 9 1 2 3 4 5 Рис. 4.9. Горизонтальное распределение серий при переходе на третий уровень
d
i
1 2 2 1 1
a
i
4 4 4 3 2 10 6 7 8 9 1 2 3 4 5 Рис. 4.10. Горизонтальное распределение при считывании десятой серии
82 Состояние последовательностей и переменных
a
i
и
d
i
при переходе на четвертый уровень показан на рис. 4.13.
d
i
0 1 1 1 1
a
i
4 4 4 3 2 13 10 11 12 6 7 8 9 1 2 3 4 5 Рис. 4.11. Горизонтальное распределение при считывании тринадцатой серии
d
i
0 0 0 0 0
a
i
4 4 4 3 2 13 14 15 10 11 12 16 6 7 8 9 17 1 2 3 4 5 Рис. 4.12. Горизонтальное распределение при считывании семнадцатой серии
d
i
4 4 3 3 2
a
i
8 8 7 6 4 13 14 15 10 11 12 16 6 7 8 9 17 1 2 3 4 5 Рис. 4.13. Горизонтальное распределение серий при переходе на четвертый уровень Замещение пустых серий реальными происходит также с учетом равномерного распределения пустых серий по всем последовательностям. Порядок записи серий на четвертом уровне (
L = 4) показан на рис. 4.14.
83 Таким образом происходит заполнение входных последовательностей, пока в исходной последовательности не закончатся все серии. В конце распределения во входных последовательностях будут содержатся реальные и пустые серии, сумма которых представляет собой одно из чисел Фибоначчи порядка
N – 2, что идеально подходит для многофазной сортировки 27 13 14 15 23 33 10 11 12 16 28 6 7 8 9 17 1 2 3 4 5 Рис. 4.14. Горизонтальное распределение серий в конце четвертого уровня При повышении уровня при горизонтальном распределении серий можно представить
d
i
, как цель, которую можно достичь, если направить серий в последовательность
i. Можно считать, что в последовательность
i сразу помещается d
i
пустых серий. Последующее распределение рассматривается, как замена пустых серий на реальные, отмечая каждую замену вычитанием 1 из переменной. Таким образом, по исчерпании исходной последовательности
d
i
будет указывать число пустых серий в последовательности
i. В общем виде алгоритм горизонтального распределения серий можно записать следующим образом
1. Цель – получить при распределении по одной серии в каждой последовательности (при распределении используются числа Фибоначчи порядка
N – 2).
2. Проводим распределение, стремясь достичь цели.
3. Если цели невозможно достичь из-за того, что серии исходной последовательности исчерпаны, заканчиваем распределение.
4. Если цель же достигнута, вычисляем следующий уровень чисел Фибоначчи. Разность между этими числами и числами на предыдущем уровне (
d
i
=
a
i
(L)
–
a
i
(L – 1)
) становится новой целью распределения. Возвращаемся к шагу 2.
84 Следует отметить, что для выходной последовательности
a
N
= 0,
d
N
= 0. После того, как был рассмотрен пример и сформулирован алгоритм распределения серий, можно написать код функции
select(), обращение к которой происходит всякий раз, когда необходимо выбрать, в какую последовательность записывать новую серию. Функция
select() должна также вычислять новую строку табл. 4.4, те. значения
a
1
(L)
, …,
a
N –1
(L)
, и новые цели
d
i
=
a
i
(L)
–
a
i
(L – 1)
. Листинг тела функции
select() приведен ниже
int i, a0;
if ((d[j]) < (d[j + 1])) j++;
else
{
if (d[j] == 0)
{
L++;
a0 = a[0];
for(i = 0; i < (N – 1); i++)
{
d[i] = a0 + a[i + 1] – a[i];
a[i] = a0 + a[i + 1];
}
}
j = 0;
}
d[j] = d[j] – 1; Переменная
j представляет собой индекс входной последовательности. Ее первоначальное значение равно нулю. Если исходной последовательностью является
f
0
, тогда начальное распределение серий может быть сформулировано следующим образом пока (не конец последовательности
f
0
)
{ вызвав функцию select(), определить, куда записать серию записать серию изв Однако при распределении серий во входные последовательности следует учесть, что, как и при записи серий в ранее разбиравшейся сортировке естественным слиянием, возможно объединение двух последовательно поступающие в одну последовательность серий. А это приведет к
85 нарушению ожидаемого числа серий. В многофазной сортировке особенно важно точное число серий в каждой последовательности. Поэтому во избежание такого случайного слияния приходится усложнять алгоритм распределения. Нужно для всех последовательностей хранить значение последнего элемента последней серии. Тогда алгоритм распределения может выглядеть так пока (не конец последовательности
f
0
)
{ вызвав функцию select(), определить, куда записать серию если (последний элемент
f
j
больше первого элемента
f
0
), тогда записать серию изв иначе записать серию изв она объединится с последней серией
f
j
если (последовательность
f
0
не закончилась, тогда записать еще одну серию изв и это будет действительно новая серия
f
j
иначе
d[j] = d[j] + 1; // d[j] необходимо увеличить на 1, так как в
// последней строке функции
select() он
// был уменьшен на 1, и теперь это
// нужно компенсировать.
} Поскольку вначале работы алгоритма распределения серий входные последовательности пусты, то и значения их последних элементов не определено. Поэтому следует сначала скопировать по одной серии в каждую последовательность, и затем уже применять только что рассмотренный алгоритм. Алгоритм многофазной сортировки При обсуждении чисел Фибоначчи мы уже коснулись самого алгоритма многофазной сортировки. Так, мы знаем, что на каждом проходе серии из
n – 1 входных файлов сливаются в один выходной файл. При этом нет необходимости использовать все серии каждого входного файла. Выходной файл заполняется сериями, количество которых равно минимальному количеству серий среди всех входных файлах. Когда серии какого- либо входного файла заканчиваются, он становится выходным файлом, и начинается новый проход. При этом все остальные файлы становятся входными.
86 Основная структура алгоритма многофазной сортировки подобна главной части программы многопутевого слияния. Она состоит из трех вложенных циклов внешний цикл сливает серии, пока не будут исчерпаны входы, внутренний сливает одиночные серии из каждой входной последовательности, а самый внутренний цикл выбирает элемент с минимальным значением и пересылает его в выходной файл. Таблица 4.6 Отличия многопутевой и многофазной сортировок Параметры
Многопутевая сортировка Многофазная сортировка Число входных последовательностей
N/2
N – 1 Число выходных последовательностей
N/2 1 Переключение входных и выходных последовательностей в конце прохода Входные и выходные последовательности меняются ролями Опустошившаяся последовательность становится выходной, остальные – входными Наличие пустых серий Отсутствуют. Алгоритмом непредусмотрены Присутствуют. Начальное распределение и сортировка без пустых серий невозможны В табл. 4.6 приведены основные отличия многопутевой и многофазной сортировок. При реализации многофазной сортировки для индексации входных и выходных последовательностей, также как и при многопутевой сортировке, используется карта индексов лент
t
i
. Слияние происходит из входных последовательностей
t
1
, …,
t
N –1
в
t
N
. Как только одна из входных последовательностей становится пустой, индексу
t
N
присваивается индекс этой последовательности. Таким образом, индекс
t
N
всегда указывает на выходную последовательность. Разумеется, предыдущее значение индекса
t
N
не теряется, поскольку последовательность, на которую он указывал, теперь становится входной и участвует в слиянии наряду с остальными последовательностями. Для индексации активных входных последовательностей используется карты индексов активных входов
ta. Эта карта содержит индексы последовательностей, которые участвуют в слиянии. Считается, что последовательность участвует в слиянии, если она не содержит пустых серий, и конец текущей рассматриваемой серии еще не наступил. Число входных последовательностей, участвующих в слиянии, или число активных входов, обозначается через
k (k ≤ N – 1).
87 Число активных входных последовательностей меняется от серии к серии. Вначале каждой серии оно определяется по числу пустых серий
d
i
: если для всех, то делаем вид, что сливаем
N – 1 пустых серий и создаем пустую серию на выходе, те. просто увеличиваем
d
N
для выходной последовательности. В противном случае сливается по одной серии из всех входных последовательностей, у которых
d
i
= 0, a
d
i
остальных последовательностей уменьшается на единицу. Это означает, что из них взято по одной пустой серии.
При многофазной сортировке уровень, вычисленный при начальном распределении, уменьшается. При этом перевычисляется и необходимое число серий а последовательности
i. Как только уровень станет равным нулю – сортировка закончена, исходная последовательность отсортирована. В общем виде алгоритм многофазной сортировки можно записать так
do // слияние изв открываем на запись последовательность
t
N
;
do // слияние одной серии
{
k = 0;
for(i = 0; i < (N – 1); i++) // определение числа активных входов
if (d[i] > 0) d[i] = d[i] – l;
else { k++; ta[k] = t[i]; }
if (k == 0) d[N] = d[N] + l;
else слияние одной реальной серии изв закрываем последовательность
t
N
; и открываем ее на чтение поворот карты индексов лент t; вычисление
a[i] для следующего уровня открываем на запись последовательность
t
N
;
L––;
// уменьшаем уровень на единицу
}
while (L > 0); отсортированный результат содержится в
t[0]; Предполагается, что вначале все
N – 1 входных последовательностей с исходными сериями находятся в состоянии чтения, а компоненты карты индексов лент
t
i
=
i.
88 Операция слияния реальной серии почти идентична той же операции в сортировке с помощью многопутевого слияния, разница только в том, что здесь алгоритм исключения последовательности несколько проще
do
{
i = 0; mx = 0; min = первый элемент последовательности с индексом первый элемент последовательности с индексом ta[i];
if (x < min) {min = x; mx = i;}
} скопировать элемент из последовательности с индексом
ta[i] в выходную последовательность, индекс которой
t[N];
if (конец серии в последовательности с индексом ta[mx])
{
ta[mx] = ta[k]; k––; } // исключаем из списка активных входов
}
while (k > 0); Поворот карт индексов последовательностей, соответствующих счетчиков
d
i
, а также перевычисление коэффициентов а при переходе на низший уровень происходит следующим образом
tn = t[N]; dn = d[N]; z = a[N – 1];
for(i = N; i > 1 ; i–– )
{
t[i] = t[i – 1]; d[i] = d[i – 1]; a[i] = a[i – 1] – z; }
t[0] = tn; d[0] = dn; a[0] = z; В заключение приведем листинг программы, реализующей алгоритм многофазной сортировки, в общем виде
int j, a[N], d[N], L;
select()
{
int i, a0;
if ((d[j]) < (d[j + 1])) j++;
else
{
if (d[j] == 0)
{
89
L++;
a0 = a[0];
for(i = 0; i < (N – 1); i++)
{
d[i] = a0 + a[i + 1] – a[i];
a[i] = a0 + a[i + 1];
}
}
j = 0;
}
d[j] = d[j] – 1;
}
main()
{
int i, z, k, x, mx, min;
int t[N], ta[N –1];
FILE *f[N], f0;
for(i = 0; i < (N – 1); i++) открыть входные последовательности
f
i на запись
for(i = 0; i < (N – 1); i++)
{
a[i] = 1; d[i] = 1; }
L = 1; j = 0; a[N] = 0; d[N] = 0; записать по одной серии в каждый из входных файлов пока (не конец последовательности
f
0
) // выполнить начальное
{ // распределение вызвав функцию select(), определить, куда записать серию если (последний элемент
f
j
больше первого элемента
f
0
), тогда записать серию изв иначе записать серию изв если (последовательность
f
0
не закончилась, тогда записать еще одну серию изв иначе
d[j] = d[j] + 1;
}
for(i = 0; i < N; i++)
t[i] = i;
90 открыть входные последовательности
f
i на чтение
do // сортировка
{
z = a[N – l]; d[N] = 0; открываем на запись последовательность
t
N
;
do // слияние одной серии
{
k = 0;
for(i = 0; i < (N – 1); i++)
if (d[i] > 0) d[i] = d[i] – l;
else { k++; ta[k] = t[i]; }
if (k == 0) d[N] = d[N] + l;
else
do // слияние одной реальной серии изв первый элемент файла с индексом ta[0]; while(
i < k)
{
i++; x = первый элемент последовательности с индексом ta[i];
if (x < min) {min = x; mx = i;}
} скопировать элемент из последовательности с индексом
ta[i] в выходную последовательность, индекс которой
t[N];
if (конец серии в последовательности с индексом ta[mx])
{
ta[mx] = ta[k]; k––; }
}
while (k > 0);
z––;
}
while (z > 0); закрываем последовательность
t
N
; и открываем ее на чтение
tn = t[N]; dn = d[N]; z = a[N – 1];
for(i = N; i > 1 ; i–– ) // поворот карты индексов лент t;
{
t[i] = t[i – 1]; d[i] = d[i – 1];
a[i] = a[i – 1] – z; } // вычисление a[i] для следующего уровня
t[0] = tn; d[0] = dn; a[0] = z; открываем на запись последовательность
t
N
;
L––;
// уменьшаем уровень на единицу
}
while (L > 0); вывод на экран или сохранение в файл отсортированной последовательности, которая теперь содержится в
t[0];
}
91 Алгоритм многофазной сортировки гораздо эффективнее рассмотренных ранее алгоритмов многопутевого, естественного и простого слияния. Его применение при сортировке последовательности позволяет значительно сократить время, необходимое для обработки данных. Однако очевидно, что реализация многофазной сортировки требует значительных трудозатрат на кодирование и отладку программного кода. Поэтому применение данной сортировки целесообразно при большом объеме данных и необходимости минимизации времени, затрачиваемого на сортировку. В том случае, если объем данных небольшой, а при современных вычислительных мощностях это может быть до нескольких сот тысяч элементов, рекомендуется использовать более простые методы внешней сортировки. Задания
1. Написать программу внешней сортировки последовательности, методом простого слияния.
2. Написать программу естественного слияния с использованием двух вспомогательных файлов.
3. Написать программу естественного слияния с использованием четырех файлов.
4. Программно реализовать алгоритмы простого и естественного слияния, и сравнить их на эффективность по количеству проходов.
5. Реализовать алгоритм многопутевого слияния с общим числом файлов не менее шести.
6. Написать программу многофазной сортировки последовательности при
N = 3, 4, 5 или 6.
7. Существует метод сортировки, похожий на многофазную сортировку каскадное слияние. При нем слияние идет так если, например, речь идет о шести последовательностях, то, начиная с идеального распределения серий на последовательностях Т, …, Т, сначала проводится пя- типутевое, слияние Т, …, Т на Т, до тех пор пока не станет пустой последовательность Т, затем (Туже не трогается) проводится четырехпуте- вое слияние на Т, затем трехпутевое на Т, двухпутевое на Т и копирование из Т в Т. Следующий проход работает по аналогичной схеме все начинается с пятипутевого слияния на Т и т. д. Хотя такая схема выглядит хуже многофазного слияния, поскольку некоторые последовательности простаивают, и есть просто операции копирования, тем не менее она, что удивительно, оказывается лучше многофазной сортировки при очень больших файлах и шести или более последовательностях. Необходимо написать программу такого каскадного слияния с четко выделенной структурой.
92 8. Допустим, используется трехфайловая многофазная сортировка, причем нам этапе создается файл с r
i
сериями длины
l
i
. Нам этапе требуется одна серия из одного файла и ни одной из двух других. Поясните, почему должно соблюдаться каждое из приведенных ниже условий а)
l
i
=
l
i – 1
+
l
i – 2
для
i ≥ 1, где l
0
и
l
–1
– длины серий в двух первоначально занятых файлах били, что эквивалентно
r
i – 2
=
r
i – 1
+
r
i
) для
i ≥ 1, где
r
0
и
r
–1
– количество серий в двух первоначально занятых файлах в)
r
n
=
r
n – 1
= 1 и, следовательно,
r
n
,
r
n – 1
, …,
r
1
образуют последовательность чисел Фибоначчи.
9. Какое дополнительное условие нужно добавить к условиям, перечисленным в упражнении 8, чтобы сделать возможным выполнение многофазной сортировки ас начальными сериями длиной 1 те б) в течение
k этапов, нос другими начальными сериями. Рекомендуется рассмотреть несколько вариантов, например,
l
n
= 50,
l
n
– 1
= 31 или
l
n
= 50,
l
n – 1
= 32.
10. Покажите, что а) для сортировки
n записей помощью любого алгоритма внешней сортировки, который использует только одну магнитную ленту в качестве устройства внешней памяти, потребуется времени порядка Об) в случае использования двух магнитных лент в качестве устройств внешней памяти достаточно будет
O(n logn) времени.
11. Некоторые операционные системы предоставляют программистам виртуальную память можно писать программы, исходя из предположения, что имеется очень большая внутренняя память, способная вместить все данные. Эта память разделена на страницы, и лишь немногие из них действительно находятся во внутренней памяти в любой момент времени, остальные же хранятся на дисках. Программисту ненужно вникать вовсе эти подробности, так как операционная система сама обо всем заботится необходимые страницы автоматически вызываются в память, когда они нужны. Может показаться, что появление виртуальной памяти приводит к отмиранию методов внешней сортировки, так как эта работа может быть выполнена просто с помощью методов, разработанных для внутренней сортировки. Проанализируйте сложившуюся ситуацию. Почему специально разработанный метод внешней сортировки может оказаться лучше применения общей техники подкачки страниц к методу внутренней сортировки
93
1 ... 4 5 6 7 8 9 10 11 ... 14
5. ОРИЕНТИРОВАННЫЕ ГРАФЫ Во многих задачах, встречающихся в компьютерных науках, математике, технических дисциплинах часто возникает необходимость наглядного представления отношений между какими-либо объектами. Ориентированные и неориентированные графы – естественная модель для таких отношений. В этой главе рассмотрены основные структуры данных, которые применяются для представления ориентированных графов, а также описаны некоторые основные алгоритмы определения связности ориентированных графов и нахождения кратчайших путей.
5.1. Основные определения Ориентированный графили сокращенно орграф)
G = (V, Е) состоит из множества вершин
V и множества дуг Е. Вершины также называют узлами, а дуги – ориентированными ребрами. Дуга представима в виде упорядоченной пары вершин (
v, w), где вершина v называется началом, a w – концом дуги. Дугу (
v, w) часто записывают как v → w и изображают в виде Говорят также, что дуга
v → w ведет от вершины v к вершине w, а вершина
w смежная с вершиной v. На рис. 5.1 показан орграф с четырьмя вершинами и пятью дугами. Рис. 5.1. Ориентированный граф Вершины орграфа можно использовать для представления объектов, а дуги – для отношений между объектами. Например, вершины орграфа могут представлять города, а дуги – маршруты рейсовых полетов самолетов из одного города в другой. В другом случаев виде орграфа может быть представлена блок-схема потока данных в компьютерной программе. В последнем примере вершины соответствуют блокам операторов программы, а дугам – направленное перемещение потоков данных.
v
w
1 2
3 4
94 Путем в орграфе называется последовательность вершин
v
1
,
v
2
, …, для которой существуют дуги
v
1
→
v
2
,
v
2
→
v
3
, …,
v
n – 1
→
v
n
. Этот путь начинается в вершине
v
1
и, проходя через вершины
v
2
,
v
3
, …,
v
n – 1
, заканчивается в вершине
v
n
. Длина пути – это количество дуг, составляющих путь, в данном случае длина пути равна
n – 1. Как особый случай пути рассмотрим одну вершину
v как путь длины 0 от вершины v к этой же вершине v. На рис. 5.1 последовательность вершин 1, 2, 4 образуют путь длины 2 от вершины 1 к вершине 4. Путь называется простым, если все вершины на нем, за исключением, может быть, первой и последней, различны. Цикл – это простой путь длины не менее 1, который начинается и заканчивается водной и той же вершине. На рис. 5.1 вершины 3, 2, 4, 3 образуют цикл длины 3. Во многих приложениях удобно к вершинами дугам орграфа присоединить какую-либо информацию. Для этих целей используется помеченный орграф, те. орграфу которого каждая дуга и/или каждая вершина имеет соответствующие метки. Меткой может быть имя, весили стоимость дуги, или значение данных какого-либо заданного типа. На рис. 5.2 показан помеченный орграф, в котором каждая дуга имеет буквенную метку. Этот орграф имеет интересное свойство любой цикл, начинающийся в вершине 1, порождает последовательность буква ив которой всегда четное количество буква и b. Рис. 5.2. Помеченный орграф В помеченном орграфе вершина может иметь как имя, таки метку. Часто метки вершин используются в качестве имен вершин. Так, на рис. 5.2 числа, помещенные в вершины, можно интерпретировать как имена вершин и как метки вершин.
1 2
4 3
a
a
a
a
b
b
b
b
95
5.2. Представления ориентированных графов Для представления ориентированных графов можно использовать различные структуры данных. Выбор структуры данных зависит от операторов, которые будут применяться к вершинами дугам орграфа. Одним из наиболее общих представлений орграфа
G = (V, Е) является матрица смежности. Предположим, что множество вершин
V = {1, 2, …, n}. Матрица смежности для орграфа
G – это матрица А размера n x n со значениями бу- левого типа, где
A[i, j] = true тогда и только тогда, когда существует дуга из вершины
i в вершину j. Часто в матрицах смежности значение true заменяется на 1, а значение
false – на 0. Время доступа к элементам матрицы смежности зависит от размеров множества вершин и множества дуг. Представление орграфа в виде матрицы смежности удобно применять в тех алгоритмах, в которых надо часто проверять существование данной дуги. Обобщением описанного представления орграфа с помощью матрицы смежности можно считать представление помеченного орграфа также посредством матрицы смежности, ноу которой элемент
A[i, j] равен метке дуги
i → j. Если дуги от вершины i к вершине j не существует, то значение
A[i, j] не может быть значением какой-либо допустимой метки, а может рассматриваться как пустая ячейка. На рис. 5.3 показана матрица смежности для помеченного орграфа, приведенного на рис. 5.2. Здесь дуги представлены меткой символьного типа, а пробел используется при отсутствии дуг. Основной недостаток матриц смежности заключается в том, что она требует
O(n
2
) объема памяти, даже если дуг значительно меньше, чем Поэтому для чтения матрицы или нахождения в ней необходимого элемента требуется время порядка
O(n
2
), что не позволяет создавать алгоритмы с временем
O(n) для работы с орграфами, имеющими порядка O(n) дуга а b
3
b а
4
b a Рис. 5.3. Матрица смежности для помеченного орграфа, изображенного на рис. 5.2 Поэтому вместо матриц смежности часто используется другое представление для орграфа
G = (V, Е, называемое представлением посредством списков смежности. Списком смежности для вершины
i называется список всех вершин, смежных с вершиной
i, причем определенным образом упорядоченный. Таким образом, орграф
G можно представить посредством массива
HEAD (заголовок, чей элемент HEAD[i] является указателем на список смежности вершины
i. Представление орграфа с помощью списков смежности требует для хранения объем памяти, пропорциональный сумме количества вершин и количества дуг. Если количество дуг имеет порядок
O(n), то и общий объем необходимой памяти имеет такой же порядок. Но и для списков смежности время поиска определенной дуги может иметь порядок
O(n), так как такой же порядок может иметь количество дугу определенной вершины. На рис. 5.4 показана структура данных, представляющая орграф из рис. 5.1 посредством связанных списков смежности. Если дуги имеют метки, то их можно хранить в ячейках связанных списков.
HEAD Списки смежности Рис. 5.4. Структура списков смежности для орграфа, изображенного на рис. 5.1
HEAD
ADJ Рис. 5.5. Представление орграфа, изображенного на рис. 5.1 Для вставки и удаления элементов в списках смежности необходимо иметь массив
HEAD, содержащий указатель на ячейки заголовков списков
2 3
#
2
#
4
#
3
#
1 2
3 4
1 2
3 4
1 2
4 4
3 0
2 3
5 0
8 3
7 0
6 2
9 0
97 смежности, ноне сами смежные вершины. Но если известно, что граф не будет подвергаться изменениям (или они будут незначительны, то предпочтительно сделать массив
HEAD массивом курсоров на массив ADJ (от
adjacency – смежность, где ячейки ADJ[HEAD[i]], ADJ[HEAD[i] + 1] и т. д. содержат вершины, смежные с вершиной
i, и эта последовательность смежных вершин заканчивается первым встреченным нулем в массиве
ADJ. Пример такого представления для графа из рис. 5.1 показан на рис. 5.5.
5.3. Задача нахождения кратчайшего пути В этом параграфе рассматриваются задачи нахождения путей в ориентированном графе. Пусть есть ориентированный граф
G = (V, Е, у которого все дуги имеют неотрицательные метки (стоимости дуга одна вершина определена как источник. Задача состоит в нахождении стоимости кратчайших путей от источника ко всем другим вершинам графа
G (здесь длина пути определяется как сумма стоимостей дуг, составляющих путь. Эта задача часто называется задачей нахождения кратчайшего пути с одним источником. Отметим, что мы будем говорить о длине пути даже тогда, когда она измеряется в других, нелинейных, единицах измерения, например во временных единицах. Можно представить орграф
G в виде карты маршрутов рейсовых полетов из одного города в другой, где каждая вершина соответствует городу, аду- га
v → w – рейсовому маршруту из города v в город w. Метка дуги v → w – это время полета из города
v в город w
2
. В этом случае решение задачи нахождения кратчайшего пути с одним источником для ориентированного графа трактуется как минимальное время перелетов между различными городами. Для решения поставленной задачи будем использовать жадный алгоритм, который часто называют алгоритмом Дейкстры (
Dijkstra). Алгоритм строит множество
S вершин, для которых кратчайшие пути от источника уже известны. На каждом шаге к множеству
S добавляется та из оставшихся вершин, расстояние до которой от источника меньше, чем для других оставшихся вершин. Если стоимости всех дуг неотрицательны, то можно быть уверенным, что кратчайший путь от источника к конкретной
1
Может показаться, что более естественной задачей будет нахождение кратчайшего пути от источника к определенной вершине назначения. Но эта задача в общем случае имеет такой же уровень сложности, что и задача нахождения кратчайшего пути для всех вершин графа (за исключением того счастливого случая, когда путь к вершине назначения будет найден ранее, чем просмотрены пути ко всем вершинам графа.
2
Можно предположить, что в данном случаев качестве модели больше подходит неориентированный граф, поскольку метки дуги могут совпадать. Но фактически в большинстве случаях время полетав противоположных направлениях между двумя городами различно. Кроме того, предположение о совпадении меток дуги не влияет (и не помогает) на решение задачи нахождения кратчайшего пути.
98 вершине проходит только через вершины множества
S. Назовем такой путь особым. На каждом шаге алгоритма используется также массив
D, в который записываются длины кратчайших особых путей для каждой вершины. Когда множество
S будет содержать все вершины орграфа, те. для всех вершин будут найдены особые пути, тогда массив
D будет содержать длины кратчайших путей от источника к каждой вершине. Ниже представлен листинг программы, реализующей алгоритм Дейкст- ры. Здесь предполагается, что в орграфе
G вершины поименованы целыми числами, те. множество вершин
V = {1, 2, …, n}, причем вершина 1 является источником. Массив С – это двумерный массив стоимостей, где элемент С, j] равен стоимости дуги
i → j. Если дуги i → j не существует, то C[i, j] принимается равным ∞, те. большим любой фактической стоимости дуг. На каждом шаге
D[i] содержит длину текущего кратчайшего особого пути к вершине i.
procedure Dijkstra;
begin
(1)
S := {1};
(2)
for i := 2 to n do
(3)
D[i] := C[1, i]; { инициализация D }
(4)
for i := 1 to n – 1 do begin
(5) выбор из множества
V \ S такой вершины w, что значение
D[w] минимально
(6) добавить
w к множеству S;
(7)
for каждая вершина v из множества V \ S do
(8)
D[v] := min(D[v], D[w] + C[w, v])
end
end; Применим алгоритм Дейкстры для ориентированного графа, показанного на рис. 5.6. Рис. 5.6. Орграф с помеченными дугами Вначале
S = {1}, D[2] = 10, D[3] = ∞, D[4] = 30 и D[5] = 100. На первом шаге цикла (строки (4) – (8) листинга)
w = 2, те. вершина 2 имеет минимальное значение в массиве
D. Затем вычисляем D[3] = min(∞, 10 + 50) = 60.
D[4] и D[5] не изменяются, так как не существует дуг, исходящих из вершины и ведущих к вершинами. Последовательность значений элементов массива
D после каждой итерации цикла показаны в табл. 5.1. Несложно внести изменения в алгоритм так, чтобы можно было определить сам кратчайший путь (те. последовательность вершин) для любой вершины. Для этого надо ввести еще один массив Р вершин, где Р содержит вершину, непосредственно предшествующую вершине
v в кратчайшем пути. Вначале положим
P[v] = 1 для всех v ≠ 1. В приведенном выше листинге после строки (8) надо записать условный оператор сусло- вием
D[w] + C[w, v] < D[v], при выполнении которого элементу P[v] присваивается значение
w. После выполнения алгоритма кратчайший путь к каждой вершине можно найти с помощью обратного прохождения по предшествующим вершинам массива Р. Таблица 5.1 Вычисления по алгоритму Дейкстры для орграфа из рис. 5.6 Итерация
S w
D[2]
D[3]
D[4]
D[5] Начало {1} –
10
∞ 30 100 1
{1,
2} 2 10 60 30 100 2
{1, 2, 4}
4 10 50 30 90 3
{1, 2, 4, 3}
3 10 50 30 60 4
{1, 2, 4, 3, 5}
5 10 50 30 60 Определим кратчайший путь на примере орграфа, показанного на рис. 5.6. Массив Р имеет следующие значения Р = 1, Р = 4, Р = 1, Р = 3. Для определения кратчайшего пути, например, от вершины 1 к вершине 5, надо отследить в обратном порядке предшествующие вершины, начиная с вершины 5. На основании значений массива Р определяем, что вершине 5 предшествует вершина 3, вершине 3 – вершина 4, а ей, в свою очередь, – вершина 1. Таким образом, кратчайший путь из вершины 1 в вершину 5 составляет следующая последовательность вершин 1, 4, 3, 5. Обоснование алгоритма Дейкстры Алгоритм Дейкстры – пример алгоритма, где жадность окупается в том смысле, что если что-то хорошо локально, то оно будет хорошо и глобально. В данном случае что-то локально хорошее – это вычисленное расстояние от источника к вершине
w, которая пока не входит в множество, но имеет кратчайший особый путь. (Напомним, что особым называется путь, который проходит только через вершины множества
S.) Чтобы понять, почему не может быть другого кратчайшего, ноне особого, пути, рассмотрим рис. 5.7. Здесь показан гипотетический кратчайший путь к вершине
w, который сначала проходит до вершины х через вершины
100 множества
S, затем после вершины х путь, возможно, несколько раз входит в множество
S и выходит из него, пока не достигнет вершины w. Но если этот путь короче кратчайшего особого пути к вершине
w, то и начальный сегмент пути от источника к вершине х (который тоже является особым путем) также короче, чем особый путь к
w. Нов таком случаев строке листинга (5) при выборе вершины
w необходимо было выбрать не эту вершину, а вершину х, поскольку D[x] меньше D[w]. Таким образом, приходим к противоречию, следовательно, не может быть другого кратчайшего пути к вершине
w, кроме особого. (Отметим здесь определяющую роль того факта, что все стоимости дуг неотрицательны, без этого свойства помеченного орграфа алгоритм Дейкстры не будет работать правильно) Для завершения обоснования алгоритма Дейкстры надо еще доказать, что
D[v] действительно показывает кратчайшее расстояние доверши- ны
v. Рассмотрим добавление новой вершины w к множеству S (строка листинга (6)), после чего происходит пересчет элементов массива
D в строках (7), (8) этого листинга при этом может получиться более короткий особый путь к некоторой вершине
v, проходящий через вершину w. Если часть этого пути до вершины
w проходит через вершины предыдущего множества
S и затем непосредственно к вершине v, то стоимость этого пути, в строке листинга (8) сравнивается си, если новый путь короче, изменяется значение
D[v]. Существует еще одна гипотетическая возможность кратчайшего особого пути к вершине
v, которая показана на рис. 5.8. Рис. 5.7. Гипотетический кратчайший путь к вершине Рис. 5.8. Реально невозможный кратчайший особый путь
Здесь этот путь сначала идет к вершине
w, затем возвращается к вершине х, принадлежащей предыдущему множеству S, затем следует к вершине
v. Но реально такого кратчайшего путине может быть. Поскольку вершинах помещена в множество S раньше вершины w, то все кратчайшие пути от источника к вершине х проходят исключительно через вершины предыдущего множества
S. Поэтому показанный на рис. 5.8 путь к вершине, проходящий через вершину w, не короче, чем путь к вершине х, про
w Множество S Источник
w
x Множество S
Источник
101 ходящий через вершины множества
S. В результате и весь путь к вершине v, проходящий через вершины хине короче, чем путь от источника к вершине х, проходящий через вершины множества S, и далее непосредственно к вершине
v. Таким образом, доказано, что оператор в строке листинга) действительно вычисляет длину кратчайшего пути. Время выполнения алгоритма Дейкстры Предположим, что алгоритм Дейкстры оперирует с орграфом, имеющим
n вершин и е дуг. Если для представления орграфа используется матрица смежности, то для выполнения внутреннего цикла строки) потребуется время
O(n), а для выполнения всех n – 1 итераций цикла строки) потребуется время порядка
O(n
2
). Время, необходимое для выполнения оставшейся части алгоритма, как легко видеть, не превышает этот же порядок. Если количество дуге значительно меньше n
2
, то лучшим выбором для представления орграфа будут списки смежности, а для множества вершин
V \ S – очередь с приоритетами, реализованная в виде частично упорядоченного дерева. Тогда время выбора очередной вершины из множества и пересчет стоимости путей для одной дуги составит O(log n), а общее время выполнения цикла строки е log n), а не O(n
2
). Строки (1) – (3) выполняются за время порядка
O(n). При использовании очередей с приоритетом для представления множества
V \ S строка
(5) реализуется посредством оператора
DELETEMIN, а каждая из n – 1 итераций цикла (4) – (6) требует времени порядка
O(log n). В результате получаем, что общее время выполнения алгоритма
Дейкстры ограничено величиной порядка е log n). Это время выполнения значительно меньше, чем
O(n
2
), когда е существенно меньше п. Нахождение кратчайших путей между парами вершин Предположим, что имеется помеченный орграф, который содержит время полета по маршрутам, связывающим определенные города. Необходимо построить таблицу, где приводилось бы минимальное время перелета из одного (произвольного) города в любой другой. В этом случае мы сталкиваемся с общей задачей нахождения кратчайших путей, те. нахождения кратчайших путей между всеми парами вершин орграфа. Более строгая формулировка этой задачи следующая есть ориентированный граф
G = (V, Е, каждой дуге
v → w этого графа сопоставлена неотрицательная стоимость С, w]. Общая задача нахождения кратчайших путей заключается в нахождении для каждой упорядоченной пары вершин (
v, w) любого пути от
102 вершины
v в вершины w, длина которого минимальна среди всех возможных путей от
v к w. Можно решить эту задачу, последовательно применяя алгоритм
Дейкстры для каждой вершины, объявляемой в качестве источника. Но существует прямой способ решения данной задачи, использующий алгоритм Флойда (
R. W. Floyd). Для определенности положим, что вершины графа последовательно пронумерованы от 1 до
n. Алгоритм Флойда использует матрицу А размерах, в которой вычисляются длины кратчайших путей. Вначале
A[i, j] = C[i, j] для всех i ≠ j. Если дуга i → j отсутствует, то
C[i, j] = ∞. Каждый диагональный элемент матрицы А равен 0. Над матрицей А выполняется n итераций. После й итерации A[i, j] содержит значение наименьшей длины путей из вершины
i в вершину j, которые не проходят через вершины с номером, большим
k. Другими словами, между концевыми вершинами пути
i и j могут находиться только вершины, номера которых меньше или равны
k. На й итерации для вычисления матрицы А применяется следующая формула
A
k
[
i, j] = min(A
k – 1
[
i, j], A
k – 1
[
i, k] + A
k – 1
[
k, j]). Нижний индекс
k обозначает значение матрицы А после й итерации. Но это не означает, что существует
n различных матриц, этот индекс используется для сокращения записи. Графическая интерпретация этой формулы показана на рис. 5.9. Рис. 5.9. Включение вершины k в путь от вершины i к вершине j Для вычисления
A
k
[
i, j] проводится сравнение величины A
k – 1
[
i, j] те. стоимость пути от вершины
i к вершине j безучастия вершины k или другой вершины с более высоким номером) с величиной
A
k – 1
[
i, k] + A
k – 1
[
k, j] стоимость пути от вершины
i до вершины k плюс стоимость пути от вершины до вершины j). Если путь через вершину k дешевле, чем A
k – 1
[
i, j], то величина
A
k
[
i, j] изменяется.
k
j
A
k – 1
[k, j]
i
A
k – 1
[i, k]
A
k – 1
[i, j]
103 На рис. 5.10 показан помеченный орграфа на рис. 5.11 – значения матрицы А после трех итераций. Рис. 5.10. Помеченный ориентированный граф
1 2 3 1 2 3 1
0 8 5 1
0 8 5 2 3 0
∞ 2 3
0 8
3
∞ 2 0 3
∞ 2 0
A
0
[i, j]
A
1
[i, j]
1 2 3 1 2 3 1
0 8 5 1
0 7 5 2
3 0 8 2
3 0 8 3
5 2 0 3
5 2 0
A
2
[i, j]
A
3
[i, j] Рис. 5.11. Последовательные значения матрицы А Равенства
A
k
[
i, k] = A
k – 1
[
i, k] и A
k
[
k, j] = A
k – 1
[
k, j] означают, что на й итерации элементы матрицы А стоящие в й строке им столбце, не изменяются. Более того, все вычисления можно выполнить с применением только одной копии матрицы А. Время выполнения программы, реализующая алгоритм Флойда, имеет порядок
O(n
3
). Доказательство правильности работы этого алгоритма выполняется с помощью математической индукции по
k, показывая, что на й итерации вершина k включается в путь только тогда, когда новый путь короче старого. Сравнение алгоритмов Флойда и Дейкстры Поскольку версия алгоритма Дейкстры с использованием матрицы смежности находит кратчайшие пути от одной вершины за время порядка п, тов этом случае применение алгоритма Дейкстры для нахождения всех кратчайших путей потребует времени порядка
O(n
3
), те. получим такой же временной порядок, как ив алгоритме Флойда. Константы пропорциональности в порядках времени выполнения для обоих алгоритмов зависят от применяемых компилятора и вычислительной машины, а также от
104 особенностей реализации алгоритмов. Вычислительный эксперимент и измерение времени выполнения – самый простой путь подобрать лучший алгоритм для конкретного приложения. Если е, количество дуг в орграфе, значительно меньше, чем п, тогда, несмотря на относительно малую константу в выражении порядка
O(n
3
) для алгоритма Флойда, рекомендуется применять версию алгоритма Дейк- стры со списками смежности. В этом случае время решения общей задачи нахождения кратчайших путей имеет порядок пе log n), что значительно лучше алгоритма Флойда, по крайней мере, для больших разреженных графов. Вывод на печать кратчайших путей
Во многих ситуациях требуется указать сам кратчайший путь от одной вершины к другой. Чтобы восстановить при необходимости кратчайшие пути в алгоритме Флойда можно ввести еще одну матрицу Р, в которой элемент
P[i, j] содержит вершину k, полученную при нахождении наименьшего значения
A[i, j]. Если P[i, j] = 0, то кратчайший путь из вершины
i в вершину j состоит из одной дуги i → j. Для вывода на печать последовательности вершин, составляющих кратчайший путь от вершины
i до вершины j, вызывается процедура path(i, j):
procedure path (i, j: integer);
var
k: integer;
begin
k := P[i, j];
if k = 0 then
return;
path (i, k);
writeln(k);
path (k, j);
end; На рис. 5.12 показана результирующая матрица Р для орграфа из рис. 5.10.
1 2
3 1 0 3
0 2 0 0
1 3 2 0
0
P Рис. 5.12. Матрица Р для орграфа, изображенного на рис. 5.10
105 Транзитивное замыкание
Во многих задачах интерес представляет только сам факт существования пути, длиной не меньше единицы, от вершины
i до вершины j. Алгоритм Флойда можно приспособить для решения таких задач. Но полученный в результате алгоритм еще до Флойда разработал Уоршелл (
S. War-
shall), поэтому будем называть его алгоритмом Уоршелла. Предположим, что матрица стоимостей С совпадает с матрицей смежности для данного орграфа
G, те. C[i, j] = 1 только в том случае, если есть дуга
i → j, и C[i, j] = 0, если такой дуги не существует. Необходимо вычислить матрицу Атакую, что A[i, j] = 1 тогда и только тогда, когда существует путь от вершины
i до вершины j длиной не менее 1 ив противном случае. Такую матрицу А часто называют транзитивным замыканием матрицы смежности. На рис. 5.13 показано транзитивное замыкание матрицы смежности орграфа из рис. 5.10.
1 2
3 1 1 1
1 2 1 1
1 3 1 1
1 Рис. 5.13. Матрица Р для орграфа, изображенного на рис. 5.10 Транзитивное замыкание можно вычислить, применяя нам шаге следующую формулу к булевой матрице А
A
k
[
i, j] = A
k – 1
[
i, j]
1 ... 6 7 8 9 10 11 12 13 14
or (A
k – 1
[
i, k] and A
k – 1
[
k, j]). Эта формула устанавливает, что существует путь от вершины
i до вершины
j, проходящий через вершины с номерами, не превышающими k, только в следующих случаях
1. Уже существует путь от вершины
i до вершины j, который проходит через вершины с номерами, не превышающими
k – 1.
2. Существует путь от вершины
i до вершины k, проходящий через вершины с номерами, не превышающими
k – 1, и путь от вершины k до вершины
j, который также проходит через вершины с номерами, не превышающими. Здесь
A
k
[
i, k] = A
k – 1
[
i, k] и A
k
[
k, j] = A
k – 1
[
k, j], и вычисления можно выполнять водной копии матрицы А Программа Warshall вычисления транзитивного замыкания показана в следующем листинге
procedure Warshall (var A: array[1..n, 1..n] of boolean;
C: array[1..n, 1..n] of boolean);
var
106
i, j, k: integer;
begin
for i := 1 to n do
for j
:= 1
to n do
A[i, j] := C[i, j];
for k: = 1 to n do
for i := 1 to n do
for j := 1 to n do
if A[i, j] = false then
A[i, j] := A[i, k] and A[k, j]
end; Нахождение центра ориентированного графа
Предположим, что необходимо найти центральную вершину в орграфе. Эту задачу также можно решить с помощью алгоритма Флойда. Однако сначала уточним термин центральная вершина. Пусть
v – произвольная вершина орграфа
G = (V, Е).Эксцентриситет или максимальное удаление вершины
v определяется как mах{минимальная длина пути от вершины
w до вершины v}
w
V Центром орграфа G называется вершина с минимальным эксцентриситетом. Другими словами, центром орграфа является вершина, для которой максимальное расстояние (длина пути) до других вершин минимально. Рассмотрим помеченный орграф, показанный на рис. 5.14. В этом графе вершины имеют следующие эксцентриситеты: Вершина Эксцентриситет
a
∞
b
6
c
8
d
5
e
7 Найти центр орграфа сравнительно просто. Пусть С – матрица стои- мостей для орграфа
G.
1. Сначала применим алгоритм Флойда к матрице С для вычисления матрицы А, содержащей все кратчайшие пути орграфа G.
2. Находим максимальное значение в каждом столбце
i матрицы А Это значение равно эксцентриситету вершины
i.
3. Находим вершину с минимальным эксцентриситетом. Она и будет центром графа
G.
107 Время выполнения этого процесса определяется первым шагом, для которого время имеет порядок
O(n
3
). Второй шаг требует времени порядка па третий – п. Рис. 5.14. Помеченный граф Матрица всех кратчайших путей для орграфа, изображенного на рис.
5.14 представлена на рис. 5.15.
a b c d e
a
0 1 3 5 7
b
∞ 0 2 4 6
c
∞ 3 0 2 4
d
∞ 1 3 0 7
e
∞ 6 8 5 0 max
∞ 6 8 5 7 Рис. 5.15. Матрица кратчайших путей Максимальные значения в каждом столбце приведены под матрицей.
5.5. Обход ориентированных графов Для решения многих задач, касающихся ориентированных графов, необходим эффективный метод систематического обхода вершин и дуг орграфов. Таким методом является так называемый поиск в глубину. Метод поискав глубину составляет основу многих других эффективных алгоритмов работы с графами. В последних двух разделах этой главы представлены различные алгоритмы, основанные на методе поискав глубину. Предположим, что есть ориентированный граф
G, в котором первоначально все вершины помечены меткой
unvisited (не посещалась. Поиск
108 в глубину начинается с выбора начальной вершины
v графа G, для этой вершины метка
unvisited меняется наметку (посещалась. Затем для каждой вершины, смежной с вершиной
v и которая не посещалась ранее, рекурсивно применяется поиск в глубину. Когда все вершины, которые можно достичь из вершины
v, будут посещены, поиск заканчивается. Если некоторые вершины остались непосещенными, то выбирается одна из них и поиск повторяется. Этот процесс продолжается до тех пор, пока обходом не будут охвачены все вершины орграфа
G. Этот метод обхода вершин орграфа называется поиском в глубину, поскольку поиск непосещенных вершин идет в направлении вперед вглубь) до тех пор, пока это возможно. Например, пусть х – последняя посещенная вершина. Для продолжения процесса выбирается какая-либо не- рассмотренная дугах у выходящая из вершины х Если вершина у уже посещалась, то ищется другая вершина, смежная с вершиной х Если вершина у ранее не посещалась, то она помечается меткой visited и поиск начинается заново от вершины у. Пройдя все пути, которые начинаются в вершине у возвращаемся в вершину х, те. в ту вершину, из которой впервые была достигнута вершина у. Затем продолжается выбор нерассмотрен- ных дуг, исходящих из вершины хи так до тех пор, пока не будут исчерпаны все эти дуги. Для представления вершин, смежных с вершиной
v, можно использовать список смежности
L[v], а для определения вершин, которые ранее посещались, – массив
mark (метка, чьи элементы будут принимать только два значения
visited и unvisited. Эскиз рекурсивной процедуры dfs (от
depth-first search – поиск в глубину, реализующей поиск в глубину, представлен в следующем листинге
procedure dfs (v: вершина
var
w: вершина
begin
(1)
mark[v] := visited;
(2)
for каждая вершина w из списка L[v] do
(3)
if mark[w] = unvisited then
(4)
dfs(w)
end; Чтобы применить эту процедуру к графу, состоящему из п вершин, надо сначала присвоить всем элементам массива
mark значение unvisited, затем начать поиск в глубину для каждой вершины, помеченной как
unvi-
sited. Описанное можно реализовать с помощью следующего кода
109
for v := 1 to n do
mark[v] := unvisited;
for v := 1 to n do
if mark[v] = unvisited then
dfs(v) Отметим, что листинг процедуры поискав глубину является только эскизом, который еще следует детализировать. Заметим также, что эта процедура изменяет только значения массива
mark. Анализ процедуры поискав глубину Все вызовы процедуры
dfs для полного обхода графа с п вершинами и е дугами, если е ≥ п, требуют общего времени порядка е. Чтобы показать это, заметим, что нет вершины, для которой процедура
dfs вызывалась бы больше одного раза, поскольку рассматриваемая вершина помечается как
visited в строке листинга (1) еще до следующего вызова процедуры dfs и никогда не вызывается для вершин, помеченных этой меткой. Поэтому общее время выполнения строки) для просмотра всех списков смежности пропорционально сумме длин этих списков, те. имеет порядок е. Таким образом, предполагая, что е ≥ п, общее время обхода по всем вершинам орграфа имеет порядок е, необходимое для просмотра всех дуг графа. Рассмотрим следующий пример. Пусть процедура
dfs применяется к ориентированному графу, представленному на рис. 5.16, начиная с вершины А Рис. 5.16. Ориентированный граф Алгоритм помечает эту вершину как
visited и выбирает вершину Виз списка смежности вершины А. Поскольку вершина В помечена как unvi-
sited, обход графа продолжается вызовом процедуры dfs(B). Теперь процедура помечает вершину В как visited и выбирает первую вершину из списка смежности вершины В. В зависимости от порядка представления вершин в списке смежности, следующей рассматриваемой вершиной может быть или вершина С, или вершина D. Предположим, что в списке смежности вершина С предшествует вершине
D. Тогда осуществляется вызов dfs(C). В списке смежности вершины С присутствует только вершина А, но она уже посещалась ранее. Поскольку все вершины в списке смежности вершины С исчерпаны, то поиск возвращается в вершину В, откуда процесс поиска продолжается вызовом процедуры
dfs(D). Вершины Аи Сиз списка смежности вершины D уже посещались ранее, поэтому поиск возвращается сначала в вершину В, а затем в вершину А. На этом первоначальный вызов
dfs(A) завершен. Но орграф имеет вершины, которые еще не посещались Е F и G. Для продолжения обхода вершин графа выполняется вызов
dfs(E). Глубинный остовный лес В процессе обхода ориентированного графа методом поискав глубину только определенные дуги ведут к вершинам, которые ранее не посещались. Такие дуги, ведущие к новым вершинам, называются дугами дерева и формируют для данного графа остовный лес, построенный методом поискав глубину, или, сокращенно, глубинный остовный лес. На рис. 5.17 показан глубинный остовный лес для графа из рис. 5.16. Здесь сплошными линиями обозначены дуги дерева. Отметим, что дуги дерева формируют именно лес, те. совокупность деревьев, поскольку методом поискав глубину к любой ранее не посещавшейся вершине можно придти только по одной дуге, а не по двум различным дугам. В добавление к дугам дерева существуют еще три типа дуг, определяемых в процессе обхода орграфа методом поискав глубину. Это обратные, прямые и поперечные дуги. Рис. 5.17. Глубинный остовной лес для орграфа, изображенного на рис 5.16 Обратные дуги (как дуга С → А на рис. 5.17) – это такие дуги, которые в остовном лесе идут от потомков к предкам. Отметим, что дуга из вершины в саму себя также является обратной дугой. Прямыми дугами называются дуги, идущие от предков к истинным потомкам, но которые не являются дугами дерева. На рис. 5.17 прямые дуги отсутствуют.
111 Дуги, такие как
D → C и G → D на рис. 5.17, соединяющие вершины, не являющиеся ни предками, ни потомками друг друга, называются поперечными дугами. Если при построении остовного дерева сыновья одной вершины располагаются слева направо в порядке их посещения и если новые деревья добавляются в лес также слева направо, то все поперечные дуги идут справа налево, что видно на рис. 5.17. Такое взаимное расположение вершин и деревьев выбрано неслучайно, так как это помогает формировать остовный лес. Как можно различить эти четыре типа дуг Легко определить дуги дерева, так как они получаются в процессе обхода графа как дуги, ведущие к тем вершинам, которые ранее не посещались. Предположим, что в процессе обхода орграфа его вершины нумеруются в порядке их посещения. Для этого в листинге процедуры поискав глубину после строки (1) надо добавить следующие строки
dfnumber[v] := count;
count := count + Назовем такую нумерацию глубинной нумерацией ориентированного графа. Всем потомкам вершины
v присваиваются глубинные номера, не меньшие, чем номер, присвоенный вершине
v. Фактически вершина w будет потомком вершины
v тогда и только тогда, когда выполняются неравенства количество потомков вершины
v
1
. Очевидно, что прямые дуги идут от вершин с низкими номерами к вершинам с более высокими номерами, а обратные дуги – от вершин с высокими номерами к вершинам с более низкими номерами. Все поперечные дуги также идут от вершин с высокими номерами к вершинам с более низкими номерами. Чтобы показать это, предположим, что есть дугах у и выполняется неравенство dfnumber(x)≤ dfnumber(y). Отсюда следует, что вершинах пройдена (посещена) раньше вершины у. Каждая вершина, пройденная в промежуток времени от вызова
dfs(x) и до завершения
dfs(y), становится потомком вершины х в глубинном остовном лесу. Если при рассмотрении дуги х → у вершина у еще не посещалась, то эта дуга становится дугой дерева. В противном случае дугах убудет прямой дугой. Таким образом, если для дуги х → у выполняется неравенство, то она не может быть поперечной дугой. Далее будет рассмотрено применение метода поискав глубину для решения различных задач на графах.
1
Для истинных потомков вершины v первое из приведенных неравенств должно быть строгим.
112
5.6. Ориентированные ациклические графы Ориентированный ациклический граф – это орграф, не имеющий циклов. Можно сказать, что ациклический орграф более общая структура, чем дерево, но менее общая по сравнению с обычным ориентированным графом. На рис. 5.18 представлены дерево, ациклический орграф и ориентированный граф с циклом. Рис. 5.18. Три ориентированных графа Ациклические ориентированные графы удобны для представления синтаксических структур арифметических выражений, имеющих повторяющиеся подвыражения. Например, на рис. 5.19 показан ациклический орграф для выражения аса+ ее. Подвыражения аи (ас встречаются в выражении несколько раз, поэтому они представлены вершинами, в которые входят несколько дуг. Рис. 5.19. Ориентированный ациклический граф для арифметического выражения Ациклические орграфы также полезны для представления отношений частичных порядков. Отношением частичного порядка
R, определенным на множестве
S, называется бинарное отношение, для которого выполняются следующие условия
113 1. Ни для какого элемента а из множества S не выполняется aRa свойство антирефлексивности).
2. Для любых ас из S, таких, что аи, выполняется ас свойство транзитивности. Двумя естественными примерами отношений частичного порядка могут служить отношение меньше чем (обозначается знаком «<»), заданное на множестве целых чисел, и отношение строгого включения (
) множеств. Например, пусть
S = {1, 2, 3} и P(S) – множество всех подмножеств множества
S, те. Отношение строгого включения (
) является отношением частичного порядка на множестве
P(S). Очевидно, включение А
Ане выполняется для любого множества А из Р (антирефлексивность), и при выполнении А
В и В
С выполняется АС (транзитивность. Ациклические орграфы можно использовать для графического изображения отношения частичного порядка между какими-либо объектами. Для начала можно представить отношение
R как одноименное множество пар (дуг) таких, что пара (а, b) принадлежит этому множеству только тогда, когда выполняется
aRb. Если отношение R определено на множестве элементов
S, то ориентированный граф G = (S, R) будет ациклическим. И наоборот, пусть
G = (S, R) является ациклическим орграфом, а R
+
– отношение, определенное следующим образом
aR
+
b выполняется тогда и только тогда, когда существует путь (длиной не менее 1) от вершины а к вершине. Тогда R
+
– отношение частичного порядка на
S. (Отношение R
+
является транзитивным замыканием отношения
R.) Рис. 5.20. Ациклический граф, представляющий отношение строгого включения В качестве примера на рис. 5.20 показан ациклический орграф (
P(S),
R), где S = {1, 2, 3}. Здесь R
+
– отношение строгого включения на множестве Проверка ацикличности орграфа Предположим, что есть ориентированный граф
G = (V, Е. Необходимо определить, является ли он ациклическим, те. имеет ли он циклы. Чтобы ответить на этот вопрос, можно использовать метод поискав глубину. Если при обходе орграфа
G методом поискав глубину встретится обратная дуга, то ясно, что граф имеет цикл. С другой стороны, если в орграфе есть цикл, тогда обратная дуга обязательно встретится при обходе этого орграфа методом поискав глубину. Чтобы доказать это, предположим, что орграф имеет цикл (рис. 5.21). Пусть при обходе данного орграфа методом поискав глубину вершина
v имеет наименьшее глубинное число среди всех вершин, составляющих цикл. Рассмотрим дугу
u → v, принадлежащую этому циклу. Поскольку вершина
u входит в цикл, то она должна быть потомком вершины
v в Рис. 5.21. Ациклический орграф, представляющий структуру предшествова- ний глубинном остовном лесу. Поэтому дуга
u → v не может быть поперечной дугой. Так как глубинный номер вершины
u больше глубинного номера вершины
v, то отсюда следует, что эта дуга не может быть дугой дерева и прямой дугой. Следовательно, дуга
u → v является обратной дугой, как показано на рис. 5.21. Топологическая сортировка Большие проекты часто разбиваются на совокупность меньших задач, которые выполняются в определенном порядке и обеспечивают полное завершение целого проекта. Например, план учебного заведения требует определенной последовательности в чтении учебных курсов. Ациклические орграфы могут служить естественной моделью для таких ситуаций. Например, когда чтение учебного курса С предшествует чтению учебного курса
D, создается дуга от курса С к курсу
D. Чтобы проиллюстрировать этот пример приведем ациклический орграф (рис.
5.22), представляющий структуру предше- ствований пяти учебных курсов. Чтение учебного курса С, например, требует предварительного чтения курсов Си С. Рис. 5.22. Ациклический орграф, представляющий структуру предшествований
Топологическая сортировка – это процесс линейного упорядочива- ния вершин ациклического орграфа таким образом, что если существует
115 дуга от вершины
i к вершине j, тов упорядоченном списке вершин орграфа вершина
i предшествует вершине j. Например, для орграфа из рис. 5.22 вершины после топологической сортировки расположатся в следующем порядке С, С, С, С, С. Топологическую сортировку легко выполнить с помощью модифицированной процедуры поискав глубину, если в ней после строки (4) добавить оператор вывода на печать
procedure topsort (v: вершина печать в обратном топологическом порядке вершин, достижимых из вершины v}
w: вершина
begin
mark[v] := visited;
for каждая вершина w из списка L[v] do
if mark[w] = unvisited then
topsort (w);
writeln (v)
end; Когда процедура
topsort заканчивает поиск всех вершин, являющихся потомками вершины
v, печатается сама вершина v. Отсюда следует, что процедура
topsort распечатывает все вершины в обратном топологическом порядке. Эта процедура работает вследствие того, что в ациклическом орграфе нет обратных дуг. Рассмотрим, что происходит, когда поиск в глубину достигает конечной вершины х. Из любой вершины могут исходить только дуги дерева, прямые и поперечные дуги. Но все эти дуги направлены в вершины, которые уже пройдены (посещались до достижения вершины хи поэтому предшествуют вершине х.
1 ... 6 7 8 9 10 11 12 13 14