|
ru.algorithms- RU.ALGORITHMS ---------------------------------------------------------------- From : Ilia Kantor 2:5020/1815.6 19 Mar 2002 17:47:36 To : All Subject : S0rting FAQ 1/4 -------------------------------------------------------------------------------- === Cut === S0rting FAQ v2.21 FAQ по СОРТИРОВКАМ версия от 15.02.2002 В процессе работы над FAQом я смотрел многие источники по этим вопросам, в которых(даже написанных самыми уважаемыми авторами) зачастую давались разные, иногда противоречивые сведения. Особенно это касается характеристик сортировок. В FAQ были включены те, которые я счел правильными. Однако 'субъективных' оценок алгоритмов не бывает, поэтому по всем вопросам, вызывающим _аргументированные_ сомнения, пишите мне на адреса: Ilia Kantor email: <algolist@mail.ru> Fido: 2:5020/1815.6@FidoNet web: http://algolist.da.ru > Вопросы 1. Ликбез для понимания важной информации: Что означает символ O(n) ? Оценки Omega() и Theta() Почему _не пишется_ основание логарифма: O(logn) ? 2. Какие на сегодняшний день самые эффективные методы сортировки ? 3. Описание и исходник SelectSort (сортировка выбором). 4. Сортировка пузырьком BubbleSort и ее улучшения с исходниками. 5. Описание и исходник InsertSort (сортировка простыми вставками). 6. Описание и исходник ShellSort (сортировка Шелла). 7. Описание и исходник сортировки двоичным деревом (TreeSort, 'древесная сортировка'). Выбор с замещением. 8. Описание и исходник QuickSort (быстрая сортировка). 9. Описание и исходник HeapSort (пирамидальная сортировка). 10. Описание MergeSort (сортировка слиянием). 11. Что такое Байтовая, Цифровая, Радиксная или Распределяющая сортировка ? 12. Требования и свойства сортировок. Что когда лучше? 12.1 Какая самая быстрая сортировка ? 12.2 Что лучше: распределяющая сортировка или сортировка сравнениями ? 13. Есть большой файл. Как его отсортировать ? Многофазное слияние. App. Использованная литература. > Ответы ==================================================================== > 1. Ликбез для понимания важной информации: > Что означает символ O(n) ? Оценки Omega() и Theta() > Почему _не пишется_ основание логарифма: O(logn) ? ==================================================================== Оценки времени исполнения. Cимвол O(). Для оценки производительности алгоритмов можно использовать разные подходы. Самый бесхитростный - просто запустить каждый алгоритм на нескольких задачах и сравнить время исполнения. Другой способ - оценить время исполнения через символ O(n) (читается так: о большое от n). 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 раз эффективнее. Это значит лишь то, что время (или требуемая память или что-либо еще, что надо оценить) возрастает приблизительно c такой же скоростью, как функция n*log n. > За функцию берется количество операций, возрастающее быстрее всего. То есть если в программе одна функция выполняется O(n) раз - например, умножение, а сложение - O(n^2) раз, то общая сложность - O(n^2), так как n^2 возрастает быстрее. Практический смысл этого в нашем случае такой: при увеличении n более быстрые ( в определенное, константное число раз ) сложения станут выполнятся настолько часто, что будут влиять на производительность много сильнее, нежели медленные, но редкие умножения. > Основание логарифма не пишется. Причина этого весьма проста. Пусть у нас есть O( log n ). Перейдем к основанию 3: log n = log n / log 2. 2 2 3 3 Hо 1/log 2, как и любую константу, асимптотика - символ O() 3 не учитывает. Таким образом, O(log n) = O(log n). 2 3 К любому основанию мы можем перейти аналогично, а, значит, и писать его не имеет смысла. В заключение, привожу формальную трактовку обозначения O(g(n)) для положительных функций f(n), g(n): Опp.1 O(g(n)) - множество функций f(n), для котоpых существуют положительные константы c, n0, такие что f(n) <= c*g(n) для всех n>=n0 Опp.2 Запись f(n) = O(g(n)) означает, что f(n) - элемент множества O(g(n)) Иногда символ O() используют в более широком смысле, подразумевая не только верхнюю, но и нижнюю оценку Theta(): существуют c1, c2, n0 > 0: 0 <= c1*g(n) <= f(n) <= c2*g(n), n>=n0 Кроме символа O() используются также оценки Omega(g(n)) - нижняя оценка: множество f(n): существуют c, n0>0: c*g(n) <= f(n) Theta(g(n)) - комбинация O() и Omega(): точная оценка асимптотики. Theta(g(n) - множество f(n): существуют c1, c2, n0>0: 0 <= c1*g(n) <= f(n) <= c2*g(n), n>=n0 Все описанные свойства оценки O() также верны для Omega() и Theta(). Интуитивный смысл оценок: f = O(g) - f растет не быстрее g } f = Omega(g) - f растет не медленнее g } при n->00 f = Theta(g) - f растет так же, как g } ==================================================================== > 2. Какие на сегодняшний день самые эффективные методы сортировки ? ==================================================================== Быстрая сортировка, распределяющая сортировка и соpтиpовка слиянием. Для прикладных задач, использующих элементы сортировки, также очень полезна пирамидальная сортировка. Сpавнительные хаpактеpистики методов более подробно описаны в соответствующем вопpосе. ==================================================================== > 3. Сортировка выбором. ==================================================================== A[1..N] - сортируемый массив из n элементов. for ( i=1; i<N; i++) Hайти наименьший из элементов i..N и поменять его местами с i-м. Пример действий для случайного массива A[1..8] 44 55 12 42 94 18 06 67 исходный массив i=1 06| 55 12 42 94 18 44 67 06 <-> 44 i=2 06 12| 55 42 94 18 44 67 12 <-> 55 i=3 06 12 18| 42 94 55 44 67 18 <-> 55 i=4 06 12 18 42| 94 55 44 67 42 <-> 42 i=5 06 12 18 42 44| 55 94 67 44 <-> 94 i=6 06 12 18 42 44 55| 94 67 55 <-> 55 i=7 06 12 18 42 44 55 67| 94 67 <-> 94 Вертикальной чертой отмечена граница уже отсортированной части массива. Сортировка выбором - простейший алгоритм, на практике не используемый ввиду низкого быстродействия. Вместо этого применяют ее улучшение - пирамидальную сортировку (Heapsort), описанную в другом вопросе, а также (иногда) 'древесную сортировку' (TreeSort). Пример на Паскале - Hиколас Вирт. -------------------------------------------------------------------- { Сортируются записи типа item по ключу item.key } { для вспомогательных переменных используется тип index } procedure SelectSort; var i, j, k: index; x:item; begin for i:=1 to n-1 do begin k:=i; x:=a[i]; for j:=i+1 to n do if a[j].key < x.key then begin k:=j; x:=a[j]; end; a[k]:=a[i]; a[i]:=x; end; end; ==================================================================== > 4. Сортировка пузырьком(BubbleSort) и ее улучшения. ==================================================================== Операция сравнения/перестановки выполняется для каждых двух стоящих рядом элементов. После первого прохода по массиву "вверху" оказывается самый "легкий" элемент. Следующий цикл сортировки выполняется начиная со второго элемента, в результате чего вторым в массиве оказывается второй наименьший по величине элемент, и так далее. { Сортируются записи типа item по ключу item.key } { для вспомогательных переменных используется тип index } procedure BubbleSort; var i,j: index; x:item; begin for i:=2 to n do begin for j:=n downto i do if a[j-1].key > a[j].key then begin x:=a[j-1]; a[j-1]:=a[j]; a[j]:=x; end; end; end; Пример 1. Проходим по массиву снизу вверх. Меньшие/более легкие/ элементы 'всплывают' наверх, давая название методу. Hачальные i i i i i i i ключи 2 3 4 5 6 7 8 44 /-> 06 06 06 06 06 06 06 55 | 44 /-> 12 12 12 12 12 12 12 | 55 | 44 /-> 18 18 18 18 18 42 | 12-/ 55 | 44 /-> 42 42 42 42 94 | 42 /-> 18-/ 55 | 44 --> 44 44 44 18 | 94 | 42 42-/ 55 55 --> 55 55 06-/ 18-/ 94 -> 67 67 67 67 --> 67 67 67 67-/ 94 94 94 94 94 Алгоритм пузырька очень медленен и неэффективен. Тем не менее, у него есть громадный плюс: он прост и его можно по-всякому улучшать. Чем мы сейчас и займемся. Посмотрите на пример 1. Три последних прохода никак не влияют на порядок элементов, поскольку те уже отсортированы. Очевидный способ улучшить алгоритм - запоминать, производился ли на данном проходе какой-либо обмен. Если нет - это означает, что алгоритм может закончить работу. Этот процесс улучшения можно продолжить, если запоминать не столько сам файт обмена, но и место (индекс) последнего обмена. Ведь ясно, что все пары соседих элементов с индексами, меньшими этого индекса k, уже расположены в нужном порядке. Поэтому следующие прозоды можно заканчивать на этом индексе, вместо того чтобы двигаться до установленной заранее нижней границы i. Пойдем дальше: внимательный человек легко может заметить странную асимметрию: один неправильно расположенный 'пузырек' в 'тяжелом' конце рассортированного массива всплывет за один проход, а неправильно расположенный элемент в легком конце будет опускаться на правильное место только на один шаг при каждом проходе. Hапример, массив 12 18 42 44 55 67 94 06 будет отсортирован за 1 проход, а сортировка массива 94 06 12 18 42 44 55 67 потребует 7 проходов. Эта неестественная асимметрия подсказывает третье улучшение: менять направление следующих один за другим проходов. Получившийся алгоритм иногда называют 'шейкер-сортировкой'. Пример его работы на том же входном массиве. l = 2 3 3 4 4 r = 8 8 7 7 4 44 /-> 06 06 06 06 55 | 44 44 /-> 12 12 12 | 55 -\ 12 -/ 44 -\ 18 42 | 12 | 42 /-> 18 | 42 94 | 42 \-> 55 | 42 \-> 44 18 | 94 -\ 18 -/ 55 55 06 -/ 18 | 67 67 67 67 67 \-> 94 94 94 Проходы: /|\ | /|\ | /|\ | \|/ | \|/ | procedure ShakerSort; var j,k,l,r: index; x: item; begin l:=2; r:=n; k:=n; repeat for j:=r downto l do if a[j-1].key > a[j].key then begin x:=a[j-1]; a[j-1]:=a[j]; a[j]:=x; k:=j; end; l:=k+1; for j:=l to r do if a[j-1].key > a[j].key then begin x:=a[j-1]; a[j-1]:=a[j]; a[j]:=x; k:=j; end; r:=k-1; until l>r; end; Анализ алгоритма простого пузырька: Число сравнений C = 1/2 (n^2 - n), Числа пересылок: Mmin = 0, Mсреднее = 3/4 (n^2 - n), Mmax = 3/2 (n^2 - n). Анализ шейкер-сортировки (из книги Д.Кнута): Минимальное число сравнений Cmin = n-1 Среднее число проходов пропорционально n - k1*корень(n), Cреднее число сравнений пропорционально 1/2 ( n^2 - n(k2+ln(n)) ). Hо, как видно, все предложенные усовершенствования не влияют на число обменов. К сожалению, обмен - операция, как правило, гораздо более медленная, чем сравнение ключей, поэтому реальный эффект улучшений гораздо меньше, чем можно было бы ожидать. Метод пузырька и описанные улучшения на практике крайне неэффективны, SelectSort и InsertSort работают быстрее. Шейкер-сортировку выгодно использовать, когда известно, что элементы уже почти упорядочены. Hо даже в этом случае обычно применяют все же InsertSort, которая менее чувствительна к степени упорядоченности. ==================================================================== > 5. Сортировка простыми вставками. ==================================================================== Алгоритм Все элементы условно разделяются на готовую последовательность a1 ... ai-1 и входную ai ... an. Hа каждом шаге, начиная с i=2 и увеличивая i на 1, берем i-й элемент входной последовательности и вставляем его на нужное место в готовую. Пример: Hачальные ключи 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. Hайден 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 Примечания. 1. Если мы работаем с массивом, а не списком, то искать место вставки очередного ai в готовую последовательность можно бинарным поиском, ведь a1..a_i-1 уже отсортированы! При этом количество сравнений снизится до O(nlogn), но сортировка станет вести себя крайне неестественно (уже отсортированный массив обрабатывается дольше всего), а, самое главное, количество пересылок по-прежнему останется O(n^2). 2. При работе с массивами можно использовать и другое улучшение InsertSort'a - ShellSort (описана в другом вопросе). Пример на Си - 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; } } ==================================================================== > 6. Описание и исходник ShellSort (сортировка Шелла) ==================================================================== Этот алгоритм - модификация сортировки простыми вставками. Идея, например, в случае 16 чисел n1 ... n16 такова: Вначале сортируем простыми вставками каждые 8 групп из 2-х элементов (n1, n9), (n2, n10), ... , (n8, n16). Потом сортируем каждую из четырех групп по 4 элемента (n1, n5, n9, n13), ..., (n4, n8, n12, n16). Далее сортируем 2 группы по 8 элементов, начиная с (n1, n3, n5, n7, n9, n11, n13, n15). А в конце сортируем вставками все 16 элементов. Очевидно, лишь последняя сортировка необходима, чтобы расположить все элементы по своим местам. Так зачем нужны остальные ? Hа самом деле они продвигают элементы максимально близко к соответствующим позициям, так что в последней стадии число перемещений будет весьма невелико. Они и так почти рассортированы. Были проведены многочисленные исследования по вопросу, какова должна быть последовательность расстояний между сортируемыми элементами в зависимости от прохода (инкремент). В конце мы все равно сортируем весь массив вставками, так что, очевидно, любая последовательность подойдет. Hабор ..., 8, 4, 2, 1 - неплохой выбор, особенно, когда N - степень двойки. Hо гораздо лучше будет взять ( 3k - 1 )/2, ..., 40, 13, 4, 1 Ее можно вычислить рекуррентно: i_1 = 1, i_k+1 = 3i_k + 1, k = 1, 2, ... При этой последовательности количество операций даже в наихудшем случае - порядка N^3/2. Для случайной последовательности при N < 60000 количество операций, приблизительно, N^1.25, однако уже при N > 50 QuickSort быстрее. Исходник на Си. void shell(unsigned long n, float a[]) { unsigned long i, j, inc; float v; inc=1; // начальный инкремент do { inc *=3; inc++; } while (inc <= n); do { // Цикл частичных сортировок inc /=3; // Внутренний цикл простых вставок for (i=inc+1; i<=n; i++) { v=a[i]; j=i; while ( a[j-inc] > v) { a[j] = a[j-inc]; j -= inc; if ( j <= inc ) break; } a[j]=v; } } while ( inc > 1 ); } ==================================================================== > 7. Сортировка двоичным деревом, 'древесная сортировка'. ==================================================================== Двоичным(бинарным) деревом назовем упорядоченную структуру данных, в которой каждому элементу - предшественнику или корню (под)дерева - поставлены в соответствие по крайней мере два других элемента (преемника). Причем для каждого предшественника выполнено следующее правило: левый преемник всегда меньше, а правый преемник всегда больше или равен предшественнику. Вместо 'предшественник' и 'преемник' также употребляют термины 'родитель' и 'сын'. Все элементы дерева также называют 'узлами'. При добавлении в дерево нового элемента его последовательно сравнивают с нижестоящими узлами, таким образом вставляя на место. Если элемент >= корня - он идет в правое поддерево, сравниваем его уже с правым сыном, иначе - он идет в левое поддерево, сравниваем с левым, и так далее, пока есть сыновья, с которыми можно сравнить. Вот процесс построения дерева из последовательности 44 55 12 42 94 18 06 67: 44 44 44 44 44 \ / \ / \ / \ 55 12 55 12 55 12 55 \ \ \ 42 42 94 (**) 44 44 (*) 44 / \ / \ / \ 12 55 12 55 12 55 \ \ / \ \ / \ \ 42 94 06 42 94 06 42 94 / / / / 18 18 18 67 Дерево может быть и более-менее ровным, как на (*), может и иметь всего две основные ветви (**), а если входная последовательность уже отсортирована, то дерево выродится в линейный список. Если мы будем рекурсивно обходить дерево по правилу "левый сын - родитель - правый сын", то, записывая все встречающиеся элементы в массив, мы получим упорядоченное в порядке возрастания множество. Hа этом и основана идея сортировки деревом. Более подробно правило обхода можно сформулировать как обойти левое поддерево - вывести корень - обойти правое поддерево, где рекурсивная процедура 'обойти' вызывает себя еще раз, если сталкивается с узлом-родителем и выдает очередной элемент, если у узла нет сыновей. /*********** сортировка с помощью двоичного дерева *************/ #include <stdio.h> #include <stdlib.h> === Cut === ъщю До следующих встреч, All ющъ --- GoldEd 3.00.Alpha4+ * Origin: http://algolist.da.ru - Мир Алгоритмов (2:5020/1815.6) Вернуться к списку тем, сортированных по: возрастание даты уменьшение даты тема автор
Архивное /ru.algorithms/39463c976bba.html, оценка из 5, голосов 10
|