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

Уханов - лекции

.pdf
Скачиваний:
10
Добавлен:
15.05.2015
Размер:
512 Кб
Скачать
☆

После первого обмена ключей местами увеличим i на единицу и будем продолжать сравнения, увеличивая i, пока не произойдет еще один обмен. Тогда опять будем уменьшать j.

2 3 8 6 4 9 5 7

i j

2 3 8 6 4 9 5 7

i j

2 3 5 6 4 9 8 7

i j

Будем продолжать процесс до тех пор, пока i не станет равно j.

2 3 5 6 4 9 8 7

i j

2 3 4 6 5 9 8 7 i j

2 3 4 5 6 9 8 7

i j

Этот процесс назовем расщеплением. Отметим, что в каждом сравнении этого примера участвует ключ 5. Вообще в каждом сравнении будет участвовать исходный ключ k1. К моменту, когда i станет равно j, исходная запись с ключом k1 займет свою окончательную позицию. Все ключи слева от k1 будут меньше, а справа больше. Ключ 5 называется точкой расщепления или ключом расщепления. Для сортировки левой и правой частей рекомендуется применить тот же метод. Таким образом, налицо рекурсия.

Пример:

Сортировка целых чисел. Процесс расщепления:

//t – сортируемый массив, i, j – расщепляемый участок.

//Функция возвращает индекс в точке расщепления

int Partition (int *t, int i, int j) { int b;

b = 1;

while (i < j) {

if (t [i] > t [j]) { b = !b;

t [i] ↔ t [j];

}

if (b) j --; else i ++;

}

return i;

}

Сортировка:

//t – сортируемый массив, i, j – расщепляемый участок void Hoar (int *t, int i, int j) {

if (! (i < j)) return; k = Partition (t, i, j); Hoar (t, i, k-1); Hoar (t, k+1, j);

}

Обращение к функции:

//t – сортируемый массив, n – размерность массива void qsort (int *t, int n) {

Hoar (t, 0, n-1);

}

Оценим трудоемкость алгоритма.

Простую прикидку числа сравнений можно произвести, если предположить, что на каждой

стадии деление массива происходит точно пополам. При разделении полной таблицы потребуется выполнить N сравнений. При работе с двумя частями разделенной таблицы потребуется 2(N/2) = N сравнений и т.д. Число дроблений таблицы, очевидно, равно log2N. Следовательно, суммарное число сравнений пропорционально N log2 N . В действительности, деление происходит

не пополам, поэтому сравнений будет больше.

Интересно, что применение алгоритма к сортированной последовательности потребует

N(N −1) ~ N2 сравнений, т.к. каждая операция расщепления становится бесполезной. Таким

2

образом, алгоритм предпочитает несортированные таблицы.

Для исправления ситуации Хоаром предложено два способа выбора ключа расщепления:

1)на отрезке (i, j) возьмем медианный ключ k(i+j)/2. Поменяем местами ki и k(i+j)/2 и начнем расщепление;

2)на отрезке (i, j) возьмем случайный ключ km, поменяем местами ki и km и начнем расщепление.

При таких манипуляциях среднее время работы алгоритма будет N log2 N .

Сортировка ненатуральных чисел

//base – базовый адрес начала таблицы

//size – размер записи в таблице

//n – количество записей в таблице

//cmp – указатель на функцию сравнения ключей двух записей

//функция сравнения должна возвращать < 0, если I < II, = 0, если I = II, > 0, если I > II void Sort (void *base, int size, int n, int (*cmp)(void*, void*)) {

int i, j, k; char s;

for (i = 0; i < n-1; i ++) { k = i;

for (j = i + 1; j < n; j ++) {

if ((*cmp)((char*)base + j*size, (char*)base + k*size) < 0) k = j;

}

for (j = 0; j < size; j ++) {

s = *((char*)base + i*size;

*((char*)base + i*size + j = *((char*)base + k*size + j; *((char*)base + k*size + j = s;

}

}

}

Разделяющие сортировки

Основная идея разделяющей сортировки заключается в следующем. Пусть А – исходное множество сортируемых ключей. Допустим нам удалось разбить это множество на попарно непересекающиеся подмножества А0, А1, …, Аm такие, что Аj<Aj+1, т.е. любой ключ из Аj меньше любого ключа из Aj+1. Если это удалось вделать, то подвергнув каждое из подмножеств такому процессу получим сортированную последовательность. К этому классу может быть отнесена сортировка Хоара.

Двоичная поразрядная сортировка

Ключ рассматривается как последовательность битов. Устанавливаем указатели на начало и конец таблицы. Двигаемся слева направо от начала в поисках ключа со старшим битом 1. Двигаемся справа налево от конца в поисках ключа со старшим битом 0. Меняем местами эти ключи. Продолжаем встречное движение по данному принципу до встречи. В результате таблица расщепляется на две части. В левой части все ключи имеют старший бит равный 0, в правой – 1. Таким образом, Aj<Aj+1. Затем эта процедура выполняется для второго бита и полученных подпоследовательностей и т.д.

Пример:

010 101 111 001 011 100

010 101 111 001 011 100

010 011 001 111 101 100

Получили разделение исходного множества ключей на два. В первом множестве собраны ключи со старшим битом, равным 0, во втором – 1. Далее нужно повторить процесс расщепления для каждого множества по второму биту.

Число расщеплений будет равно log2N. Каждый раз будем просматривать N элементов. Таким образом, время сортировки пропорционально N log2 N .

Функция расщепления.

//t – указатель на начало таблицы

//l, r – границы расщепляемого участка

//keylen – длина ключей

//nombit – номер бита, по которому происходит расщепление

//возвращает индекс самого правого ключа с битом nombit равным нулю int BitPart (char *t, int keylen, int l, int r, int nombit) {

char *lrec, *rrec;

// указатели на левую и правую записи соответственно

int lb, rb;

// биты левой и правой записи с номером nombit

while (l < r) {

 

lrec = t + l*keylen;

// смещаемся в t на l*keylen байт

rrec = t + r*keylen;

// смещаемся в t на r*keylen байт

lb = GiveBit (lrec, nombit); rb = GiveBit (rrec, nombit); if (!lb && !rb) l ++;

if (!lb && rb) { l ++;

r --;

}

if (lb && !rb) {

l ++; r --;

Change (lrec,rrec);

}

if (lb && rb) r --;

}

if (lb && rb) return r-1; else return r;

}

Функция сортировки.

//выполняет сортировку участка от l до r таблицы t с длинной ключа

//keylen по биту, начиная с nombit

void BSort (char *t, int l, int r, int keylen, int nombit) {

int k;

 

if (!(l < r)) return;

// условие завершения прохода для данного nombit

if (nombit / 8 > keylen) return;

// условие завершения по количеству nombit

k = BitPart (t, keylen, l, r, nombit);

 

BSort (t, l, k, keylen, nombit+1);

 

BSort (t, k+1, r, keylen, nombit+1);

 

}

Обращение к функции BSort. // n – число ключей

void BitSort (char *t, int keylen, int n) { BSort (t, 0, n-1, keylen, 0);

}

static unsigned char bits [ ] = {128, 64, 32, 16, 8, 4, 2, 1};

// record – указатель на запись, из которой выделяем один бит int GiveBit (char *record, int nombit) {

int NomByte; int BitInByte;

NomByte = nombit / 8; BitInByte = nombit % 8;

return (record [NomByte] & bits [BitInByte]) >> BitInByte;

}

Цифровая поразрядная сортировка по принципу «сначала по младшей цифре»

Возьмем в нулевое множество все ключи с младшим битом 0, в множество с индексом 1, ключи с младшим битом 1 и т.д. Затем выполним подсчет ключей в множествах. Таким образом, получим С – массив счетчиков, в котором С [i] – это число ключей со старшей цифрой i. После подсчета ясно, что все ключи, имеющие младшую цифру 0, должны помещаться, начиная с нулевой позиции, ключи с цифрой 1 – начиная с позиции С [0], с цифрой 2 – с позиции С [0] + C [1] и т.д. Разместим ключи в соответствии с описанным правилом. Таким образом, добились разделения Ai<Ai+1. Далее повторим процесс по следующей цифре и т.д.

Рассмотрим на примере ключей в троичной системе счисления. Дано множество ключей:

102 211 012 020 201 111 121 022

Для троичной системы счисления размерность массива счетчиков будет три. Тогда С0 будет содержать 1 (имеется только один элемент, с 0 в старшем бите – 020), С1 будет содержать 4 (211, 201, 111, 121), и, наконец, С2 содержит 3 (102, 012, 022). Расставим теперь ключи в соответствии с

номерами счетчиков 0, 1, 2. Получим

 

 

020

211

201

111

121

102

012

022.

Снова повторяем процесс расщепления по следующей цифре.

C0

= 2

(201, 102);

С1

= 3

(211, 111, 012);

С2

= 3

(020, 121, 022).

Расставляем

 

 

 

 

 

201

102

211

111

012

020

121

022.

Последнее расщепление для последней цифры.

С0

= 3

(012, 020, 022);

 

 

 

С1

= 3

(102, 111, 121);

 

 

 

С2

= 2

(201, 211).

 

 

 

 

Расставляем в соответствии с номерами

012

020

022

102

111

121

201

211.

Получена окончательная сортированная последовательность ключей.

Можно брать любую систему счисления. Удобно, если цифра – бит. Поэтому обычно используются либо 16-ричная (один бит – цифра), либо 256-ричная (один байт – цифра).

Функция, выполняющая сортировку. #define NDIG 256

void DigitalSort(char *t, int N, int KeyLen){

//t - сортируемый массив

//N - его длина

//KeyLen - число цифр в сортируемом ключе

int *Count;

 

// массив счетчиков

 

int Dig;

 

// текущая цифра в ключе

 

int i,j;

 

// переменные циклов

 

char *b;

 

// буферная область

 

int *Pos;

// позиции расстановки

 

Count=(int *)calloc(NDIG, sizeof(int));

 

Pos=(int *)calloc(NDIG, sizeof(int));

 

b=calloc(N,KeyLen);

 

 

// цикл по всем цифрам в ключе

 

for(i=0; i<KeyLen; i++){

 

memset(Count,0, NDIG*sizeof(int));

// обнуляем массив счетчиков

for(j=0; j<N; j++){

 

// для каждого ключа

Dig=*(t+j*KeyLen+KeyLen-i-1); // получаем i-ю цифру j-го ключа

Count[Dig]++;

// считаем количество ключей с такой i-й цифрой

}

 

 

 

//расчет позиций, откуда будут начинаться записи с соответствующими цифрами

Pos[0]=0;

for(j=1; j<NDIG; j++){ Pos[j]=Pos[j-1]+Count[j-1];

}

//расстановка, в соответствии с номером позиции

for(j=0; j<N; j++){ Dig=*(t+j*KeyLen+KeyLen-i-1);

memcpy(b+KeyLen*Pos[Dig]++, t+j*KeyLen, KeyLen);

}

memcpy(t,b,KeyLen*N); // копирование буфера обратно в t и по следующей цифре

}

free(b);

free(Count);

free(Pos);

}

Сортировки слиянием

Слияние означает объединение двух или более упорядоченных таблиц в одну упорядоченную.

Двухпутевое слияние

Пусть имеется две отсортированные последовательности

1 2 4 7

3 5 8 11.

Требуется получить единую сортированную последовательность.

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

12 4 7

3 5 8 11;

1 2

4 7

 

 

 

3 5 8

11;

1 2 3

4

7

 

 

5

8

11;

1 2 3 4 5 7 8 11.

Это двухпутевое слияние. В зависимости от числа сливаемых таблиц может быть многопутевым.

Простейшая форма сортировки слиянием начинает с подмножеств, состоящих их единичных элементов и объединяет их попарно в N/2, если N четно и N/2+1, если N нечетно, сортированных подмножеств, содержащих по два элемента. Затем эти подмножества снова попарно сливаются, их число снова уменьшается вдвое и т.д., пока таблица не будет отсортирована полностью.

Проиллюстрируем алгоритм на примере с использованием двух областей.

 

23

15

4

4

4

15

23

8

6

6

8

4

15

8

8

4

8

23

11

11

6

6

6

15

15

11

11

11

17

16

24

17

17

23

17

17

24

24

24

23

51

16

16

16

24

16

51

51

51

51

Все слияния, выполняемые при переходе из одной области в другую, требуют не более чем N сравнений. Необходимо примерно log2N таких переходов. Следовательно, время работы алгоритма пропорционально N log2 N .

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

Сортировка фон Неймана

Пусть А исходная область памяти. Ключи в этой области имеют частичную упорядоченность. Устанавливаем указатели на начало и конец области.

 

 

 

 

 

 

 

a

c

e

 

f

d

b

кусок

возрастания

ключей

 

кусок

убывания

ключей

Выполняем слияние левого и правого участков упорядоченности (а и b). Результат слияния помещаем в начало области B. Двигаясь в достигнутых позициях в А выполняем слияние следующих участков с и d. Результат помещаем в B справа налево от конца. Получаем

 

 

 

 

 

 

 

a + b

e + f

 

c + d

Снова перекачиваем в А и приступаем к сортировке. Так до тех пор, пока не будет получен единый участок сортировки.

Напишем функцию слияния двух отрезков.

//а – исходная область – массив целых ключей

//b – область, куда будет помещен результат слияния

//l – указатель на начало левого участка упорядоченности;

//по окончанию работы указатель на начало следующего участка

//r – указатель на начало правого участка упорядоченности;

//по окончанию работы указатель на начало следующего участка

//k – указатель на позицию в b, начиная с которой помещаем в b результат слияния

//step – шаг движения по b (1 или –1).

void Join (int *a, int *b, int *l, int *r, int *k, int step){

int bl;

// индикатор окончания левого участка упорядоченности

int br;

// индикатор окончания правого участка упорядоченности

int f;

// результат сравнения l-го и r-го элементов

bl = 0;

// участки упорядоченности не окончены

br = 0;

// если bl или br будет равно 1, значит соответствующий участок окончен

//цикл до тех пор, пока указатель на левый участок меньше указателя на правый

//и не закончился хотя бы один участок упорядоченности

while (*l < *r && !(bl&&br)) {

bl = a [*l+1] < a [*l];

// проверяем, не окончен ли участок

br = a [*r-1] < a [*r];

 

f = a [*l] < a [*r];

 

if (!bl && (br || f)) {

// если левый участок не окончен, а правый окончен или a [l] < a [r]

b [*k] = a [*l];

// в b помещаем элемент слева

(*l) ++;

// изменяем указатель на левый участок

} else {

 

//bl || (!br && !f)

//если левый участок окончен или правый не окончен и a [l] > a [r]

b [*k] = a [*r];

// в b помещаем элемент справа

(*r) --;

// сдвигаем правый указатель

}

 

(*k)+ = step;

// изменяем указатель на позицию в b

}

}

Функция, осуществляющая один проход по множеству ключей.

//а – исходная область – массив целых ключей

//b – область, куда будут помещены результаты слияния

//n – размеры областей

//возвращает число отсортированных отрезков в области b int OnePass (int *a, int *b, int n) {

int l, r;

// начала сливаемых участков

int kl, kr;

// указатели на позиции в b, куда будем помещать результаты слияния

int step;

// если 1 – помещаем в начало b, если -1 – в конец

int count;

// количество отсортированных отрезков

int *k;

// указатель на то, какую позицию kl или kr считать текущей в b

count = 0;

 

 

kl = l = 0;

 

 

kr = r = n-1;

 

 

step = 1;

 

 

// цикл до тех пор, пока не просмотрим все множество ключей

while (l < r) {

 

 

if (step > 0) k = &kl;

// если нужно помещать в начало b, то левая позиция текущая

else k = &kr;

 

// иначе – правая

Join (a, b, &l, &r, k, step);

// выполняем слияние отрезков упорядоченности

count ++;

// увеличиваем число обработанных отрезков

step = - step;

// переключаем положение текущей позиции в b

}

 

 

return count;

 

 

}

Функция сортировки

//a – сортируемый массив

//n – размерность массива void Neumann (int *a, int n) {

int *b;

// дополнительная область для сортировки

int *a1, *b1;

// указатели на области a и b

int f;

// индикатор, указывающий в какой области происходит сортировка

b = (int *)calloc(n, sizeof(int));

// распределение памяти под дополнительную область

f = 1;

 

 

do {

 

 

if (f) {

 

 

a1 = a;

// нужно сортировать а

b1 = b;

// результат помещать в b

} else {

 

 

a1 = b;

// сортируем b

b1 = a;

// результат помещаем в а

}

// переключаем индикатор

f = !f;

} while (OnePass (a1, b1, n) > 1);

// цикл до тех пор, пока число сортируемых участков > 1

// если f = 0, то сортировка закончилась в b

if (!f) memcpy (a, b, n*sizeof(int));

// копирование массива из а в b

free (b);

 

}

Если таблица случайна, то в ней около N/2 упорядоченных участков, так как ситуация, что ki > ki+1 равновероятна ситуации ki < ki+1. При каждом просмотре множества ключей число отрезков упорядоченности сокращается вдвое за счет слияния. Таким образом, число просмотров в среднем равно log2N. При каждом просмотре требуется переписать все N записей в другую область. Следовательно, время сортировки пропорционально N log2 N .

Внешние сортировки

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

Метод пузырька

Исходная таблица располагается в файле на диске.

Разобьем всю внешнюю таблицу на 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

12

6

13

Выбираем наименьшее 6 и записываем на выход. На место 6 подкачиваем следующую

запись. Это 18, больше чем 6,

следовательно, этой записи разрешено участвовать в выборе.

Получаем:

 

 

 

17

12

18

13

Снова выбираем наименьшее 12 и на выход. На его место подкачиваем 9, меньше чем 12, следовательно в выборе не участвует. Отметим этот факт скобками и т.д.

17

(9)

18

13

17

(9)

18

(11)

(4)

(9)

18

(11)

(4)(9) (2) (11)

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

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.

Процесс фибоначчиева слияния можно проиллюстрировать таблицей:

f1

f2

f3

число обработанных отрезков

21*1

13*1

–

34

8*1

–

13*2

26

–

8*3

5*2

24