
Метод пузырька
Исходная таблица располагается в файле на диске.
Разобьем всю внешнюю таблицу на N зон так, чтобы в оперативную память можно было поместить две зоны. Считываем первые две зоны и выполняем внутреннюю сортировку. У получившегося множества нижнюю половину записываем в первую зону. На освободившееся место подкачиваем третью зону, сортируем и нижнюю половину переписываем во вторую зону. На освободившееся место подкачиваем четвертую зону и т.д.
В результате, когда пройдем весь файл, в верхней половине будут собраны наибольшие ключи всей таблицы (т.е. пузырек). Эти ключи перепишем в последнюю N зону. Повторим этот процесс для зон от 1 до N-1.
Время сортировки пропорционально N2.
Двухпутевое слияние
Используются многие файлы, расположенные по возможности на разных дисках.
Пусть требуется отсортировать таблицу из 500 записей, а пространство в оперативной памяти есть только под 100 записей. Рассмотрим сортировку на трех файлах.
f1, f2, f3 – используемые файлы.
Будем читать исходный файла по 100 записей за раз. Для прочитанных записей будем выполнять внутреннюю сортировку и результат поочередно дописывать в файлы f1 и f2, пока не исчерпаются входные данные. Эта начальная распределительная фаза процесса сортировки поместит пять отрезков в файлы следующим образом:
f1: записи 1-100, 201-300, 401-500;
f2: записи 101-200, 301-400;
f3: пусто.
Таким образом, получили в файлах f1, f2 группы сортированных кусков. Выполняем внешнее двухпутевое слияние файлов f1 и f2 в файл f3. Получаем:
f1: пусто;
f2: пусто;
f3: записи 1-200, 201-400, 401-500.
Получили отрезки вдвое более длинные, чем исходные. Поочередно копируем отрезки в файлы f1 и f2. Вновь повторяем процесс слияния. Так до тех пор, пока не получим единственный отрезок в файле.
Время сортировки пропорционально log2S, где S – первоначальное число отрезков.
Структуру программы можно улучшить, если уменьшить число копирований данных. Это можно выполнить следующим образом. Распределить отрезки поочередно на f1 и f2. Выполнить их слияние на f3. Затем половину отрезков с f3 копировать на f1. Выполнить слияние f1 и f3 на f2. Снова половину отрезков с f2 копировать на f1 и т.д. Таким образом, число копирований уменьшается в два раза.
Порождение начальных отрезков
Рассмотрим процесс порождения начальных отрезков, которые затем подвергаются операции слияния. С помощью этого процесс можно получить длину начального отрезка значительно больше объема доступной оперативной памяти. Эта возможность является очень важной, поскольку время внешней сортировки слиянием в значительной мере зависит от числа начальных отрезков.
Имеется память под N записей. Читаем первые N записей, отыскиваем среди них запись с наименьшим ключом и помещаем ее на выход. На ее место подкачивается новая запись из входного файла, причем если ее ключ больше, чем ключ только что выведенной записи, то она будет участвовать в выборе, в противном случае не будет. Снова выбираем запись с наименьшим ключом и т.д. Когда в памяти не останется ни одной записи, которой разрешено участвовать в выборе, порождение отрезка закончено и начинается порождение следующего отрезка.
Пример:
Имеем последовательность
17 12 6 13 18 9 11 4 2 8 26 5 7 19 29 41
и имеем место в оперативной памяти под 4 записи.
Читаем первые четыре записи:
17 Выбираем запись. Это 18, Получаем:
17
12 6
наименьшее больше чем
13 6 и записываем на выход. На место 6 подкачиваем следующую 6, следовательно, этой записи разрешено участвовать в выборе.
12 18 13
17 (9)
17 (9)
(4)
(9) (4) (9) Получили,
что
18
18
18
(2)
13
(И) (И) (И)
все записи не участвуют в выборе. Следовательно, порождение первого
участка завершено. Теперь разрешим этим записям участвовать в выборе.
4 9 2 11
4 9 8 11
26 9 8 11
26 9 (5) 11
26 (7) (5) 11
26 (7) (5) 19
26 (7) (5) 29
41 (7) (5) 29
41 (7) (5)
(7) (5)
7 5 Выход:
6 12 13 17 18 2 4 8 9 11 19 26 29 41 5 7
Таким образом, получили три начальных отрезка. Причем длина отрезков больше объема оперативной памяти.
Фибоначчиево слияние Копирования при слиянии можно устранить полностью, если начать с Fn отрезков в файле f1 и с Fn-1 отрезков в файле f2, где Fn и Fn-1 – последовательные числа Фибоначчи. Числа Фибоначчи: F0 = 0; F1 = 1; F2 = F1 + F0;
Fn = Fn-1 + Fn-2 + ... + F0. 0 1 1 2 3 5 8 13 21 34 ...
Рассмотрим множество из 34 начальных отрезков. Распределим отрезки по двум файлам f1 и f2 и будем выполнять слияние с использованием файла f3.
Процесс фибоначчиева слияния можно проиллюстрировать таблицей:
fl |
f2 |
f3 |
число обработанных отрезков |
21*1 |
13*1 |
- |
34 |
8*1 |
— |
13*2 |
26 |
- |
8*3 |
5*2 |
24 |
5*5 |
3*3 |
- |
25 |
2*5 |
— |
3*8 |
24 |
- |
2*13 |
1*8 |
26 |
1*21 |
1*13 |
— |
21 |
- |
- |
1*34 |
34 |
Элемент таблицы А*В означает А отрезков условной длины В.
Копирования исключаются за счет точного распределения отрезков по файлам. На примере использовали двухпутевое слияние. Если использовать многопутевое слияние, то будет работать быстрее. Поэтому рассмотрим слияние на шести файлах для 129 начальных отрезков.
фаз а |
fi |
f2 |
f3 |
и |
fs |
f6 |
обработано отрезков |
1 |
31*1 |
30*1 |
28*1 |
24*1 |
16*1 |
- |
129 |
2 |
15*1 |
14*1 |
12*1 |
8*1 |
— |
16*5 |
80 |
3 |
7*1 |
6*1 |
4*1 |
- |
8*9 |
8*5 |
72 |
4 |
3*1 |
2*1 |
— |
4*17 |
4*9 |
4*5 |
68 |
5 |
1*1 |
- |
2*33 |
2*17 |
2*9 |
2*5 |
66 |
6 |
— |
1*65 |
1*33 |
1*17 |
1*9 |
1*5 |
65 |
7 |
1*129 |
- |
- |
- |
- |
- |
129 |
Для того, чтобы заставить многопутевое слияние работать так, как показано на примере, необходимо иметь точное фибоначчиево распределение отрезков по файлам. Его можно получить, если прокручивать рассмотренную схему в обратном порядке. Рассмотрим таблицу в обратной последовательности, игнорируя выходной файл.
уровень |
fl |
f2 |
f3 |
U |
fs |
сумма |
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 |
6 |
31 |
30 |
28 |
24 |
16 |
129 |
|
|
|
|
|
|
|
n |
an |
b„ |
cn |
d„ |
en |
to |
n+1 |
an + bn |
an + cn |
an + dn |
an + en |
an |
to + 4an |
Из таблицы следует, что
en = an-1;
dn = an-1 + en-1 = an-1 + an-2;
cn = an-1 + dn-1 = an-1 + an-2 + an-3;
bn = an-1 + cn-1 = an-1 + an-2 + an-3 + an-4;
an = an-1 + bn-1 = an-1 + an-2 + an-3 + an-4 + an-5,
где a0 = 1 и an = 0, при n < 0.
Числа Фибоначчи порядка p определяются правилами:
Fn(p) = Fn-1(p) + Fn-2(p) + ... + Fn-p(p) при n ≥ p;
Fn(p) = 0 при 0 ≤ n ≤ p-2;
Fp-1(p) = 1.
Другими словами, начинаем с p-1 нулей, затем пишем 1. Каждое следующее число является суммой p предыдущих. При p = 2 получаем обычную последовательность Фибоначчи.
Выведенная формула для an показывает, что число отрезков в f1 в процессе слияния на шести файлах есть число Фибоначчи пятого порядка: an = Fn+4(5).
Числа Фибоначчи пятого порядка образуют следующую последовательность:
0 0 0 0 1 1 2 4 8 16 31 ...
Итак, будем вычислять an и tn+1. Как только будет получено tn+1 ≥ N, где N – общее число отрезков, то получим нужное нам точное распределение отрезков. Если получено tn+1 > N, то значит N не является точным числом Фибоначчи. Поэтому следует дополнить N фиктивными нулевыми отрезками.
В общем случае, для M сливаемых файлов число отрезков в файле f1 будет числом Фибоначчи порядке p = M-1: an = Fn+p-1(p). А общее число отрезков будет вычисляться по формуле: tn+1 = tn + (p-1)an.
Каскадное слияние Каскадное слияние подобно многофазному начинается с точного распределения отрезков по лентам, хотя правила распределения другие. В качестве примера рассмотрим слияние, использующее шесть файлов.
№ |
fi |
f2 |
f3 |
f4 |
fs |
f6 |
обработано отрезков |
1 |
55*1 |
50*1 |
41*1 |
29*1 |
15*1 |
- |
190 |
2 |
— |
5*1 |
9*2 |
12*3 |
14*4 |
15*5 |
190 |
3 |
5*15 |
4*14 |
12*3 |
2*9 |
1*5 |
- |
190 |
4 |
— |
1*15 |
1*29 |
1*41 |
1*50 |
1*55 |
190 |
5 |
1*190 |
- |
- |
- |
- |
- |
190 |
Каждая строка представляет полный проход по всем данным. Проход 2, например, получен из 1 выполнением 5-путевого слияния с f1, ..., f5, на f6, пока не опустеет f5, затем 4-путевого слияния с f1, ..., f4 на f5, 3-путевого – с f1, f2, f3 на f4, 2-путевого – с f1, f2 на f3 и, наконец, однопутевого (копирования) с f1 на f2. Подробно получение второго прохода можно представить в таблице.
fi |
f2 |
f3 |
и |
fs |
f6 |
55*1 |
50*1 |
41*1 |
29*1 |
15*1 |
- |
40*1 |
35*1 |
26*1 |
14*1 |
— |
15*5 |
26*1 |
21*1 |
12*1 |
- |
14*4 |
- |
14*1 |
9*1 |
— |
12*3 |
— |
— |
5*1 |
- |
9*2 |
- |
- |
- |
- |
5*1 |
- |
- |
- |
- |
Ясно, что операции копирования излишни, и их можно было бы опустить. Фактически копирование занимает лишь небольшой процент времени. Большая часть времени тратится на пяти и четырехпутевые слияния.
Рассматривая процесс в обратном порядке и игнорируя выходной файл, можно вывести точные распределения отрезков по лентам на любом этапе. Числа в распределении носят название каскадных.
уровень |
fi |
f2 |
f3 |
и |
fs |
0 |
1 |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
2 |
5 |
4 |
3 |
2 |
1 |
3 |
15 |
14 |
12 |
9 |
5 |
4 |
55 |
50 |
41 |
29 |
15 |
5 |
190 |
175 |
146 |
105 |
55 |
... |
... |
... |
... |
... |
... |
n |
an |
b„ |
Cn |
d„ |
en |
n+1 |
an + bn + + cn + + dn + en |
an + bn + + cn + dn |
an + bn + + cn |
an + bn |
an |
Вычисляем an до тех пор, пока не получим an ≥ N. В этом случае будет получена требуемое точное распределение отрезков по файлам. Если получено an > N, то требуется дополнить N фиктивными нулевыми отрезками.