Главная > Программирование > Общие алгоритмы и методики > |
FAQ по сортировкам |
Секция 1 из 2 - Предыдущая - Следующая
From: Ilia Kantor <Ilia.Kantor@p6.f1815.n5020.z2.fidonet.org> Date: Fri, 26 Jan 2001 16:55:14 +0300 Sorting FAQ V1.1 FAQ по СОРТИРОВКАМ версия 1.1 > Вопросы 1. Ликбез для понимания важной информации: Что означает символ O(n) ? Почему _не пишется_ основание логарифма: O ( log n ) ? 2. Какие на сегодняшний день самые эффективные методы сортировки ? 3. Описание и исходник InsertSort (сортировка простыми вставками). 4. Описание и исходник QuickSort (быстрая сортировка). 5. Описание и исходник HeapSort (пирамидальная сортировка). 6. Требования QuickSort и HeapSort. InsertSort... Что лучше ? 7. Какая самая быстрая сортировка ? 8. Что такое Байтовая, Цифровая, Радиксная или Распределяющая сортировка ? 9. Что лучше: распределяющая сортировка или быстрая ? 10. Есть большой файл. Как его отсортировать ? Сортировка слиянием. > Ответы ==================================================================== > 1. Ликбез для понимания важной информации: Что означает символ O(n) ? > Почему _не пишется_ основание логарифма: O ( log n ) ? ==================================================================== A: Kantor Ilia - Tomas Niemann. Оценки времени исполнения. Cимвол O(). Для оценки производительности алгоритмов можно использовать разные подходы. Самый бесхитростный - просто запустить каждый алгоритм на нескольких задачах и сравнить время исполнения. Другой способ - оценить время исполнения. Например, мы можем утверждать, что время поиска есть O(n) (читается так: о большое от n). Когда используют обозначение O(), имеют в виду не точное время исполнения, а только его предел сверху, причем с точностью до постоянного множителя. Когда говорят, например, что алгоритму требуется время порядка O(n^2), имеют в виду, что время исполнения задачи растет не быстрее, чем квадрат количества элементов. Чтобы почувствовать, что это такое, посмотрите на таблицу, где приведены числа, иллюстрирующие скорость роста для нескольких разных функций. n log n n*log n n^2 1 0 0 1 16 4 64 256 256 8 2,048 65,536 4,096 12 49,152 16,777,216 65,536 16 1,048,565 4,294,967,296 1,048,476 20 20,969,520 1,099,301,922,576 16,775,616 24 402,614,784 281,421,292,179,456 Если считать, что числа в таблице соответствуют микросекундам, то для задачи с 1048476 элементами алгоритму с временем работы O(log n) потребуется 20 микросекунд, а алгоритму с временем работы O(n^2) - более 12 дней. > Если оба алгоритма, например, O ( n*log n ), то это отнюдь > не значит, что они одинакого эффективны. Символ О не учитывает константу, то есть первый может быть, скажем в 1000 раз эффективнее. Это значит лишь то, что их время возрастает приблизительно как функция n*log n. > За функцию берется количество операций, возрастающее быстрее всего. То есть если в программе одна функция выполняется O(n) раз - например, умножение, а сложение - O(n^2) раз, то общая сложность - O(n^2), так как в конце концов при увеличении n более быстрые ( в определенное, константное число раз ) сложения станут выполнятся настолько часто, что будут тормозить куда больше, нежели медленные, но редкие умножения. > Основание логарифма не пишется. Причина этого весьма проста. Пусть у нас есть O(log2_n). Но log2_n = log3_n / log3_2, а log3_2, как и любую константу, асимптотика - символ О() не учитывает. Таким образом, O(log2_n) = O( log3_n). К любому основанию мы можем перейти аналогично, а, значит, и писать его не имеет смысла. ==================================================================== > 2. Какие на сегодняшний день самые эффективные методы сортировки ? ==================================================================== A: Kantor Ilia быстрая сортировка, распределяющая сортировка и быстрая сортировка с составными ключами, которая слишком сложна для ФАКа. Для прикладных задач, использующих элементы сортировки также очень полезна пирамидальная сортировка. ==================================================================== > 3. Сортировка простыми вставками. ==================================================================== A: Kantor Ilia - Nicolas Virt - Tomas Niemann ;-)) Алгоритм Все элементы условно разделяются на готовую последовательность a1 ... ai-1 и входную ai ... an. На каждом шаге, начиная с i=2 и увеличивая i на 1, берем i-й элемент входной последовательности и вставляем его на нужное место в готовую. Пример: Начальные ключи 44 \\ 55 12 42 94 18 06 67 i = 2 44 55 \\ 12 42 94 18 06 67 i = 3 12 44 55 \\ 42 94 18 06 67 i = 4 12 42 44 55 \\ 94 18 06 67 i = 5 12 42 44 55 94 \\ 18 06 67 i = 6 12 18 42 44 55 94 \\ 06 67 i = 7 06 12 18 42 44 55 94 \\ 67 i = 8 06 12 18 42 44 55 67 94 \\ При поиске подходящего места удобно 'просеивать' x, сравнивая его с очередным элементом ai и либо вставляя его, либо пересылая ai направо и продвигаясь налево. Просеивание может кончиться при двух условиях: 1. Найден ai с ключом, меньшим x. 2. Достигнут конец готовой последовательности. Метод хорош устойчивостью сортировки, удобством для реализации в списках и, самое главное, естественностью поведения. То есть уже частично отсортированный массив будут досортирован им гораздо быстрее чем многими 'продвинутыми' методами. Анализ Число сравнений минимальное: n - 1 среднее: ( n^2 + n - 2 ) / 4 максимальное: ( n^2 + n ) * / 2 - 1 Количество пересылок минимальное: 2 * ( n - 1 ) среднее: ( n^2 + 9 * n - 10 ) / 4 максимальное: ( n^2 + 3 * n - 4 ) / 2 Пример на Си - Tomas Niemann. -------------------------------------------------------------------- typedef int item; /* Тип сортируемых элементов */ typedef int tblIndex; /* Тип ключей, по которым сортируем */ #define compGT(a,b) (a > b) /* Функция сравнения */ void insertSort(T *a, tblIndex lb, tblIndex ub) { item t; tblIndex i, j; /*********************** * сортируем a[lb..ub] * ***********************/ for (i = lb + 1; i <= ub; i++) { t = a[i]; /* Сдвигаем элементы вниз, пока */ /* не найдем место вставки. */ for (j = i-1; j >= lb && compGT(a[j], t); j--) a[j+1] = a[j]; /* вставка */ a[j+1] = t; } } ==================================================================== > 4. Описание и исходник QuickSort (быстрая сортировка). ==================================================================== Основной алгоритм Выберем случайным образом какой-то элемент х и просмотрим массив, двигаясь слева направо, пока не найдем аi больший x, а затем справа налево, пока не найдем аi меньший х. Поменяем их местами и продолжим процесс просмотра с обменом, пока просмотры не встретятся где-то в середине массива. В результате массив разделится на две части: левую - с ключами, меньшими х и правую - с ключами, большими х. Этот шаг называется разделением. Х - центром. К получившимся частям рекурсивно применяем ту же процедуру. В результате получается очень эффективная сортировка Пример рекурсивной QuickSort ------------------------------------ typedef int item; /* Тип сортируемых элементов */ typedef int tblIndex; /* Тип ключей, по которым сортируем */ #define CompGT(a,b) (a > b) tblIndex partition(T *a, tblIndex lb, tblIndex ub) { item t, pivot; tblIndex i, j, p; /********************************** * разделение массива a[lb..ub] * **********************************/ /* Выбираем центр - pivot */ p = lb + ((ub - lb)>>1); pivot = a[p]; a[p] = a[lb]; /* сортируем lb+1..ub относительно центра */ i = lb+1; j = ub; while (1) { while (i < j && compGT(pivot, a[i])) i++; while (j >= i && compGT(a[j], pivot)) j--; if (i >= j) break; t = a[i]; a[i] = a[j]; a[j] = t; j--; i++; } /* центр в a[j] */ a[lb] = a[j]; a[j] = pivot; return j; } void quickSort(T *a, tblIndex lb, tblIndex ub) { tblIndex m; /************************** * сортируем a[lb..ub] * **************************/ while (lb < ub) { /* сортировка вставками для малых массивов */ if (ub - lb <= 12) { insertSort(a, lb, ub); return; } /* разделение пополам */ m = partition (a, lb, ub); /* Уменьшаем требования к памяти: */ /* меньший сегмент сортируем первым */ if (m - lb <= ub - m) { quickSort(a, lb, m - 1); lb = m + 1; } else { quickSort(a, m + 1, ub); ub = m - 1; } } } Улучшения На практике для увеличения быстроты, но не асимптотики, можно произвести несколько улучшений: 1. В качестве центрального для функции partition выбирается элемент, расположенный в середине. Такой выбор улучшает оценку среднего времени работы, если массив упорядочен лишь частично. Наихудшая для этой реализации ситуация возникает в случае, когда каждый раз при работе partition в качестве центрального выбирается максимальный или минимальный элемент. 1' Можно выбрать средний из первого, последнего и среднего элементов. Maxim Razin: Тогда количество проходов уменьшится в 7/6 раз. 2. Для коротких массивов вызывается сортировка вставками. Из-за рекурсии и других "накладных расходов" быстрый поиск оказывается не столь уж быстрым для коротких массивов. Поэтому, если в массиве меньше 12 элементов, вызывается сортировка вставками. Пороговое значение не критично - оно сильно зависит от качества генерируемого кода. 3. Если последний оператор функции является вызовом этой функции, говорят о хвостовой рекурсии. Ее имеет смысл заменять на итерации - в этом случае лучше используется стек. 4. После разбиения сначала сортируется меньший раздел. Это также приводит к лучшему использованию стека, поскольку короткие разделы сортируются быстрее и им нужен более короткий стек. Требования к памяти уменьшаются с n до log n. Пример, входящий в стандартную реализацию Си использует многие из этих улучшений. Стандартная реализация итерационной QuickSort ------------------------------------------------ #include <limits.h> #define MAXSTACK (sizeof(size_t) * CHAR_BIT) static void exchange(void *a, void *b, size_t size) { size_t i; /****************** * обменять a,b * ******************/ for (i = sizeof(int); i <= size; i += sizeof(int)) { int t = *((int *)a); *(((int *)a)++) = *((int *)b); *(((int *)b)++) = t; } for (i = i - sizeof(int) + 1; i <= size; i++) { char t = *((char *)a); *(((char *)a)++) = *((char *)b); *(((char *)b)++) = t; } } void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)) { void *lbStack[MAXSTACK], *ubStack[MAXSTACK]; int sp; unsigned int offset; lbStack[0] = (char *)base; ubStack[0] = (char *)base + (nmemb-1)*size; for (sp = 0; sp >= 0; sp--) { char *lb, *ub, *m; char *P, *i, *j; lb = lbStack[sp]; ub = ubStack[sp]; while (lb < ub) { /* выбираем центр и меняемся с 1м элементом */ offset = (ub - lb) >> 1; P = lb + offset - offset % size; exchange (lb, P, size); /* разделение в два сегмента */ i = lb + size; j = ub; while (1) { while (i < j && compar(lb, i) > 0) i += size; while (j >= i && compar(j, lb) > 0) j -= size; if (i >= j) break; exchange (i, j, size); j -= size; i += size; } /* центр в A[j] */ exchange (lb, j, size); m = j; /* Меньший сегмент продолжаем обрабатывать, больший - в стек */ if (m - lb <= ub - m) { if (m + size < ub) { lbStack[sp] = m + size; ubStack[sp++] = ub; } ub = m - size; } else { if (m - size > lb) { lbStack[sp] = lb; ubStack[sp++] = m - size; } lb = m + size; } } } } ==================================================================== > 5. Описание и исходник HeapSort (пирамидальная сортировка). ==================================================================== A: Kantor Ilia - Nicolas Virt Назовем пирамидой последовательность элементов hl , hl+1 , ... , hr такую, что hi <= h2i hi <= h2i+1 для всякого i = l , ... , r/2 Геометрическая интерпретация пиромиды: h1 / \ / \ / \ / \ / \ h2 h3 / \ / \ / \ / \ h4 h5 h6 h7 / \ / \ / \ / \ h8 h9 h10 h11 h12 h13 h14 h15 Для последовательности 06 42 12 55 94 18 44 06 / \ / \ / \ / \ / \ 42 12 / \ / \ / \ / \ 55 94 18 44 Добавление элемента в готовую пирамиду 1. Новый элемент Х помещаем в вершину дерева. 2. Смотрим на элемент слева и элемент справа - выбираем наименьший. 3. Если этот элемент меньше Х - меняем их местами и идем у шагу 2. Иначе конец процедуры. Фаза 1: построение пирамиды Пусть дан массив h1 ... hn . Ясно, что элементы hn/2 + 1 ... hn уже образуют 'нижний ряд' пирамиды, так как не существует индексов i , j : j = 2*i ( или j = 2*i + 1 ). То есть тут упорядочивания не требуется. На каждом шаге добавляется новый элемент слева и 'просеивается' на место. Вот иллюстрация процесса для пирамиды из 8-и элементов: 44 55 12 42 // 94 18 06 67 44 55 12 // 42 94 18 06 67 44 55 // 06 42 94 18 12 67 44 // 42 06 55 94 18 12 67 // 06 42 12 55 94 18 44 67 Фаза 2: сортировка Для того, чтобы рассортировать элементы, необходимо выполнить n шагов просеивания: после каждого шага очередной элемент берется с вершины пирамиды. На каждом шаге берем последний элемент Х, верхний элемент пирамиды помещается на его место, а Х просеивается на свое 'законное'. В этом случае необходимо совершить n - 1 шагов. Пример: 06 42 12 55 94 18 44 67 // 12 42 18 55 94 67 44 // 06 18 42 44 55 94 67 // 12 06 42 55 44 67 94 // 18 12 06 44 55 94 67 // 42 18 12 06 55 67 94 // 44 42 18 12 06 67 94 // 55 44 42 18 12 06 94 // 67 55 44 42 18 12 06 Ну вот мы м получили отсортированную последовательность, только в обратном порядке. Изменяя сравнения на противоположные, получаем функцию Heapsort на Паскале Прекрасной характеристикой этого метода является то, что среднее число пересылок - (n*log n)/2 и отклонения от этого значения сравнительно малы. { сортируем массив типа 'item' по ключу 'a.key' } procedure Heapsort; var i,l: index; x: item; procedure sift; label 13; var i, j: index; begin i := l; j := 2*i; x := a[i]; while j <= r do begin if j < r then if a[j].key < a[j+1].key then j := j+1; if x.key >= a[j].key then goto 13; a[i] := a[j]; i := j; j := 2*i end; 13: a[i] := x end; begin l := (n div 2) + 1; r := n; while l > 1 do begin l := l - 1; sift end; while r > 1 do begin x := a[l]; a[l] := a[r]; a[r] := x; r := r - 1; sift end end { heapsort } ============================================================== > 6. Требования QuickSort и HeapSort. InsertSort... Что лучше ? ============================================================== A: Kantor Ilia > Простые вставки. Общее быстродействие - O( n^2 ), но ввиду простоты метода, является наибыстрейшим для малых ( 12-20 элементов ) массивов. Естественное поведение. Легко прибавлять новые элементы. Ввиду своих особенностей, чрезвычайно хорош для списков. Сортировка не требует дополнительной памяти. > Быстрая сортировка Общее быстродействие - O( nlogn ). Случай n^2 теоретически возможен, но крайне [ 1 / (n^logn) ] маловероятен. В общем и целом - наиболее быстрая сортировка сравнениями для разупорядоченных массивов с >50 элементами. Поведение неестественно. Уже практически отсортированный массив будет сортироваться столько же, сколько и полностью разупорядоченный. Итерационный вариант требует logn памяти, рекурсивный - O(n). > Пирамидальная сортировка. В 1.5 раза медленнее быстрой. Наихудшего случая нет - всегда O(nlogn). Практически, ее элементы часто применяются в смежных задачах. Поведение неестественно. Основное достоинство - сортировка не требует дополнительной памяти. ==================================================================== > 7. Какая самая быстрая сортировка ? ==================================================================== A: Kantor Ilia Давайте разберемся раз и навсегда. Есть два типа сортировок: Распределяющая и ее вариации | Сортировка сравнениями | Основана на том, что число возможных | Пользуется лишь возможностью значений ключа конечно. | прямого сравнения ключей и | их упорядоченностью. | Quicksort, Heapsort... Для сортировок сравнениями давно доказана теорема о максимальном быстродействии: O( nlogn ). Для сортировок распределением это - O(n). Быстрее сортировать нельзя. Итак, наибыстрейшие сортировки - Quicksort - быстрая, Radix sort - распределяющая и их молодой гибрид: Multiway Quicksort /MQS / - быстрая с составными ключами. апример, для строк. Вообще, нужно смотреть по данным и имеющимся в наличии ресурсам, но в типичных случаях наибыстрейшими решениями станут <см выше>. =========================================================================== > 8. Что такое Байтовая, Цифровая, Радиксная или Распределяющая сортировка > ? =========================================================================== A: Kantor Ilia К сожалению, или к счастью, единица информации в современной технике способна применять лишь 2 значения: 1 и 0. А значит, любые компьютерные данные тоже могут принимать ограниченное количество значений, так как состоят из некоторого числа битов ;-)). Пусть имеем максимум по k байт в каждом ключе ( хотя, за элемент сортировки вполне можно принять и что-либо другое. k должно быть известно заранее, до сортировки. Разрядность данных ( количество возможных значений элементов k) - m. Если мы сортируем слова, то элемент сортировки - буква. m = 33. Если в самом длинном слове 10 букв, k = 10. Обычно мы будем сортировать данные по ключам из k байт, m=255. ---------------------------------------------------------------------- Пусть у нас есть массив source из n элементов по одному байту в каждом. Для примера можете выписать на листочек массив source = { 7,9,8,5,4,7,7 }, и проделать с ним все операции, имея в виду m=9. I. Составим таблицу распределения. В ней будет m ( = 256 ) значений и заполняться она будет так: for ( i = 0 ; i < 255; i++ ) distr[i]=0; for ( i = 0 ; i < n ; i++ ) distr[source[i]] = distr[[i]] + 1; Для нашего примера будем иметь distr = { 0, 0, 0, 0, 1, 1, 0, 3, 1, 1 }, то есть i-ый элемент distr[] - количество ключей со значением i. Заполним таблицу индексов: int index[256]; index [0]=0; for ( i = 1 ; i < 255; i++ ) index[i]=index[i-1]+distr[i-1]; В index[i] мы поместили информацию о будущем количестве символов в отсортированном массиве до символа с ключом i. Например, index[8] = 5 : имеем 4, 5, 7, 7, 7, 8. А теперь заполняем новосозданный массив sorted размера n: for ( i = 0; i < n ; i++ ) { sorted[ index[ source[i] ] ]=source[i]; // попутно изменяем index уже вставленных символов, чтобы // одинаковые ключи шли один за другим: index[ source[i] ] = index[ source[i] ] +1; } Итак, мы научились за O ( n ) сортировать байты. А от байтов до строк и чисел - 1 шаг. Пусть у нас в каждом числе - k байт. Будем действовать в десятичной системе и сортировать обычные числа ( m = 10 ). сначала они в сортируем по младшему на один беспорядке: разряду: выше: и еще раз: 523 523 523 088 153 153 235 153 088 554 153 235 554 235 554 523 235 088 088 554 Ну вот мы и отсортировали за O ( k*n ) шагов. Если количество возможных различных ключей ненамного превышает общее их число, то поразрядная 'сортировка' оказывается гораздо быстрее даже 'быстрой сортировки' ! Реализация на C++ для long int'ов. Сам не делал - валялась в и-нете. #include <iostream.h> #include < stdlib.h> #include < string.h> void radix (int byte, long N, long *source, long *dest) { long count[256]; long index[256]; memset (count, 0, sizeof (count)); for ( int i=0; i<N; i++ ) count[((source[i])>>(byte*8))&0xff]++; index[0]=0; for ( i=1; i<256; i++ ) index[i]=index[i-1]+count[i-1]; for(i=0;i<N;i++) dest[index[((source[i])>>(byte*8))&0xff]++]=source[i]; } void radixsort (long *source, long *temp, long N) { radix (0, N, source, temp); radix (1, N, temp, source); radix (2, N, source, temp); radix (3, N, temp, source); } void make_random (long *data, long N) { for ( int i=0; i < N; i++ ) data[i]=rand()|(rand()<<16); } long data[10000]; long temp[10000]; void main (void) { make_random(data, 10000); radixsort (data, temp, 10000); for ( int i=0; i<100; i++ ) cout << data[i] << '\n'; } =========================================================================== > 9. Что быстрее: распределяющая сортировка или QuickSort ? =========================================================================== A: Kantor Ilia Когда количество возможных различных ключей ненамного больше их общего числа - распределяющая. Различных ключей очень много, размер массива сравнительно небольшой - быстрая. =========================================================================== > 10. Есть большой файл. Как его отсортировать ? =========================================================================== A: Kantor Ilia - Tomas Niemann Многофазная сортировка Этот тип сортировки относится к так называемым 'сортировкам слиянием'. Слиянием называется процесс объединения нескольких упорядоченных серий в одну. Пример для 3-х серий, слияемых на 4-ю: 3 7 9 3 7 9 3 7 9 7 9 7 9 { 2 4 6 1 { 2 4 6 1 2 { 4 6 1 2 3 { 4 6 1 2 3 4 { 6 1 5 8 5 8 5 8 5 8 5 8 7 9 7 9 9 1 2 3 4 5 { 6 1 2 3 4 5 6 { 8 1 2 3 5 6 7 { 8 1 2 3 4 5 6 7 8 9 { 8 Каждый pаз мы беpем свеpху наименьший элемент. Таким образом, каждая операция слияния серий требует n пересылок элементов, где n - общее число элементов серий. Пусть у нас имеется N лент: N - 1 входная и одна пустая. Мы будем слиять элементы со входных лент на выходную, пока какая-либо из них не опустеет. Затем она станет входной. Пример сортировки с шестью лентами, содержащими всего 65 серий. Серии обозначены буквами fi, в таблице - количество элементов. Тип f1 f2 f3 f4 f5 f6 16 15 14 12 8 8 7 6 4 0 8 4 3 2 0 4 4 2 1 0 2 2 2 1 0 1 1 1 1 0 1 0 0 0 0 В каждый момент времени слияние происходит на пустую ленту с остальных, поэтому число требующихся проходов приблизительно равно log N n. В данном примере распределение начальных серий побрано искусственно. Для идеальной сортировки исходные числа серий должны быть суммами n - 1 , n - 2 , ... , 1 последовательных чисел Фибоначчи порядка n - 2. Число Фибоначчи порядка p определяются следующим образом: fi+1(p) = fi(p) + fi-1(p) + ... + fi-p(p) для i >=p, fp(p) = 1, fi(p) = 0 для 0 <= i < p. Очевидно, обычные числа Фибоначчи имеют порядок 1. Поэтому предположим существование фиктивных серий, таких что сумма фиктивных с реальными дает идеальное число. Сначала все данные располагаются на одной ленте. Лента читается и отрезки распределяются по другим лентам, имеющимся в системе. после того, как созданы начальные отрезки, они сливаются, как описано выше. Один из методов, который можно использовать для создания начальных отрезков, состоит в чтении порции записей в память, их сортировке и записи результата на ленту. Выбор с замещением позволяет нам получать более длинные отрезки. Этот алгоритм работает с буфером, располагающимся в оперативной памяти. Сначала мы заполняем буфер. Затем повторяем следующие шаги до тех пор, пока не будут исчерпаны входные данные: * Выбрать запись с наименьшим ключом, т.е. с ключом, значение которого >= значения ключа последней прочитанной записи. * Если все "старые" ключи меньше последнего ключа, то мы достигли конца отрезка. Выбираем запись с наименьшим ключом в качестве первого элемента следующего отрезка. * Записываем выбранную запись. * Заменяем выбранную и записанную запись на новую из входного файла. На следующей таблице выбор с замещением иллюстрируются для совсем маленького файла. Начало файла - справа. Чтобы упростить пример, считается, что в буфер помещается всего лишь 2 записи. Конечно, в реальных задачах в буфер помещаются тысячи записей. Мы загружаем буфер на шаге В и записываем в выходной файл запись с наименьшим номером >= 6 на шаге D. Ею оказалась запись с ключом 7. Теперь мы заменяем ее на новую запись из входного файла - с ключом 4. Процесс продолжается до шага F, где мы оказывается, что последний записанный ключ равен 8 и все ключи меньше 8. В этот момент мы заканчиваем формирование текущего отрезка и начинаем формирование следующего. Шаг Вход Буфер Выход A 5-3-4-8-6-7 B 5-3-4-8 6-7 C 5-3-4 8-7 6 D 5-3 8-4 7-6 E 5 3-4 8-7-6 F 5-4 3 | 8-7-6 G 5 4-3 | 8-7-6 H 5-4-3 | 8-7-6 Обратите внимание мы храним записи в буфере до тех пор, пока не наступит время записать их в выходной файл. Если вход случаен, средняя длина отрезков равна примерно удвоенной длине буфера. Однако, если данные хоть как-то упорядочены, отрезки могут быть очень длинными. Вот почему этот метод, вообще говоря, более эффективен промежуточных, частичных сортировок. Прочитав из входного файла очередную запись, мы ищем наименьший ключ, который >= последнего считанного. При этом мы, конечно, можем просто сканировать записи в буфере. Однако, если таких записей тысячи, время поиска может оказаться недопустимо большим. Если на этом этапе использовать двоичные деревья, нам понадобится всего лишь log n сравнений. Реализация В реализации внешней сортировки на ANSI-C функция makeRuns вызывает readRec для чтения очередной записи. В функции readRec используется выбор с замещением (с двоичными деревьями) для получения нужной записи, а makeRuns распределяет записи согласно ряду Фибоначчи. Если количество отрезков оказывается вне последовательности Фибоначчи, в начало каждого файла добавляются пустые отрезки. Затем вызывается функция mergeSort, которая производит многофазное слияние отрезков. ----------------------------------------------------------- /* внешняя сортировка */ #include <stdio.h> #include <stdlib.h> #include <string.h> /* темплейт для временных файлов (формат 8.3) */ #define FNAME "_sort%03d.dat" #define LNAME 13 /* операторы сравнения */ #define compLT(x,y) (x < y) #define compGT(x,y) (x > y) /* определяем сортируемые записи */ #define LRECL 100 typedef int keyType; typedef struct recTypeTag { keyType key; /* ключ, по которому сортируем */ #if LRECL char data[LRECL-sizeof(keyType)]; /* остальные поля */ #endif } recType; typedef enum {false, true} bool; typedef struct tmpFileTag { FILE *fp; /* указатель на файл */ char name[LNAME]; /* имя файла */ recType rec; /* последняя прочитанная запись */ int dummy; /* номер холостых проходов */ bool eof; /* флаг конца пробега end-of-file */ bool eor; /* флаг конца прохода end-of-run */ bool valid; /* верно, если запись - годная */ int fib; /* идеальное число Фибоначчи */
Секция 1 из 2 - Предыдущая - Следующая
Вернуться в раздел "Общие алгоритмы и методики" - Обсудить эту статью на Форуме |
Главная - Поиск по сайту - О проекте - Форум - Обратная связь |