Из журнала Info Guide #9, Рязань, 08.2006 О сортировке Alone Coder Пусть элементы массива a нумеруются от 0 до num-1 и нам надо отсортировать их по возрастанию (сначала самые маленькие по значению, потом всё больше и больше - и так до максимального). 0 Вот тупая сортировка "каждый с каждым", которая почему-то иногда удостаивается ме- ста в литературе: for (i = 0; i < num; i++) { for (j = 0; j < num; j++) { if (a[i] > a[j]) swap(a[i], a[j]) } } Здесь n*n проходов внутреннего цикла. Вообще странно, кому пришло в голову срав- нивать каждую пару элементов два раза. Здесь результативны проходы только для i>j, а их более ранние двойники бесполезны - они сортируют в обратную сторону. Но ес- ли мы уберём эти проходы-двойники, то по- лучим ускорение всего в два раза, а дальше никак. Это тупик. 1 Меняем алгоритм так, чтобы он обменивал только соседние элементы массива. При этом ускоряем его в те же 2 раза (будет (n-1)* *(n-1)/2 проходов),используя факт,что мак- симальный элемент окажется в конце массива после первого же перебора ("всплывает"). Получается так называемая "пузырьковая со- ртировка" - bubble sort. for (i = num; i > 0; i--) { for (j = 0; j < i; j++) { if (a[j] > a[j+1]) swap(a[j], a[j+1]) } } Число обменов равно числу перемещений элементов, делённому на 2. Число перемеще- ний чисел равно количеству элементов, ум- ноженному на среднее число перемещений для элемента. Число перемещений элемента равно: число перемещений влево плюс число перемещений вправо. (Да-да! Элемент, прежде чем найдёт своё место в массиве, может двигаться как пьяный матрос! Пример - элемент 2 в масси- ве 4 2 1 3.) Элемент [i] перемещается влево столько раз, сколько бОльших его элементов было слева в первоначальном массиве a[i]. В среднем это будет i/2. Элемент [i] перемещается вправо сто- лько раз, сколько меньших его элементов было справа в первоначальном массиве a[i]. В среднем это будет (num-i-1)/2. Итого среднее число перемещений элемен- та: (num-1)/2. Отсюда среднее число пе- ремещений элементов: n*(num-1)/2. Отсюда среднее число обменов: n*(num-1)/4, т.е. примерно вдвое меньше, чем число проходов внутреннего цикла. Вот как выполняет наш алгоритм типичный процессор (reg - это регистры): for (reg_i = num; reg_i > 0; reg_i--) { for (reg_j = 0; reg_j < reg_i; reg_j++) { reg_aj=a[reg_j]; reg_aj1=a[reg_j+1]; if (reg_aj > reg_aj1) { reg_temp=a[reg_j]; a[reg_j]=a[reg_j+1]; a[reg_j+1]=reg_temp } } } В среднем 2+(4/2)=4 обращения к массиву за шаг! НАМ СТОЛЬКО НЕ НУЖНО!!! 2 Убираем 2 из 4 обращений к массиву вну- три обмена. for (reg_i = num; reg_i > 0; reg_i--) { for (reg_j = 0; reg_j < reg_i; reg_j++) { reg_aj=a[reg_j]; reg_aj1=a[reg_j+1]; if (reg_aj > reg_aj1) { a[reg_j]=reg_aj1; a[reg_j+1]=reg_aj } } } 3 Убираем одно из 2 обращений к памяти вне обмена. for (reg_i = num; reg_i > 0; reg_i--) { reg_aj=a[0]; for (reg_j1=1; reg_j1<=reg_i; reg_j1++) { reg_aj1=a[reg_j1]; //!!! if (reg_aj > reg_aj1) { a[reg_j1]=reg_aj1; a[reg_j1-1]=reg_aj; }; else reg_aj=reg_aj1; } } Теперь на каждом шаге мы обращается к массиву в среднем 2 раза. Всего-навсего. Всего-навсего? Подумайте хорошенько!!! ПОЧЕМУ НЕ 1 РАЗ?! Зачем нужна эта чехарда обменов,если нам надо всего лишь протащить максимальные значения в конец массива??? Достаточно обменивать максимальное значе- ние с последней ячейкой - и мы добьёмся того же самого! 4 Убираем обмен, чтобы обращаться к мас- сиву только 1 раз на каждом шаге. Заодно исчезает декремент. Получаем сортировку прямым поиском. for (reg_i = num; reg_i > 0; reg_i--) { reg_j=0; //current max index reg_aj=a[0]; //current max number for (reg_j1=1; reg_j1<=reg_i; reg_j1++) { reg_aj1=a[reg_j1]; if (reg_aj > reg_aj1) { reg_j=reg_j1; //current max index reg_aj=reg_aj1; //current max number }; }; a[reg_j]=a[reg_i]; a[reg_i]=reg_aj; //current max number } Итого мы получили по сравнению с перво- начальным методом ускорение в 8 (восемь) раз по числу обращений к массиву ((num-1)* *(num-1)/2) и в 2 (два) раза по числу сра- внений (тоже (num-1)*(num-1)/2). Правда, мы не учли, что в каждом элеме- нте массива может быть (и обычно есть) ещё какая-либо информация, кроме значения (value), по которому мы сортируем. Напри- мер, для построения дерева Хаффмана элеме- нты должны содержать частоту (это и будет value) и - в качестве довеска - код симво- ла. Для Z-сортировки полигонов value - это Z-координата, а остальные координаты - до- весок. Везде,где встречается "перемещение" или "обмен" элементов,имеется в виду пере- мещение ВСЕЙ информации, содержащейся в элементах. Вышеприведённые программы будут работать только если операция сравнения сравнивает values элементов, а операция присваивания копирует элемент целиком. Но тогда reg-переменные скомпилируются уже не как регистры, а как переменные в памяти - это медленно! 5 Ускоряем: for (reg_i = num; reg_i > 0; reg_i--) { reg_j=0; //current max index reg_aj=a[0].value; //current max number for (reg_j1=1; reg_j1<=reg_i; reg_j1++) { reg_aj1=a[reg_j1].value; if (reg_aj > reg_aj1) { reg_j=reg_j1; //current max index reg_aj=reg_aj1; //current max number }; }; swap(a[reg_j], a[reg_i]) } При реализации этого алгоритма на ассе- мблере, в случае частой сортировки малень- ких массивов, можно учесть, что на момент команды swap у нас уже есть в регистрах value элемента a[reg_j]. 6 Если мы уверены, что num достаточно ве- лик,то мы можем использовать более сложные алгоритмы. Например, в алгоритме heap sort (если это не лучший из алгоритмов внутренней со- ртировки, то, во всяком случае, он проще и быстрее,чем знаменитый quick sort) имеется два этапа: 1) Превращаем массив в почти полное би- нарное дерево,где каждый родитель не мень- ше потомка. Требует до (num/2)*log2(num/2) проходов, каждый из которых имеет 2.5 сра- внений, 2 обращения к массиву, 2 инкремен- та и одно умножение на 2. 2) Для j=num-1 до 1 повторяем следующую операцию: обмен первого элемента массива с j-м (исключается из дерева), потом исправ- ление получившегося дерева. Каждое исправ- ление требует до log2((j-1)/2) проходов того же вида, что и в первом этапе. Итого требуется где-то до num*(log2(num)-1) проходов указанного вида. Пусть num=32. Тогда для heap sort тре- буется до 256 обращений к массиву, до 320 сравнений,до 256+ инкрементов и до 128 ум- ножений на 2. А для старого алгоритма (см. параграф 5) - 496 обращений к массиву, 496 сравнений и 496+ инкрементов. Вот статья про heap sort из журнала Pixelate #5 (перевожу с английского, воль- но исправляя и дополняя). Автор - Robert J Ohannessian. ------------------------------------------ Heap, используемый в heap sort, - раз- новидность бинарного дерева, являющегося left-complete (т.е. заполняемого слева на- право,причём переход на новый ярус не про- исходит,пока не заполнен предыдущий). Каж- дый узел heap'а должен содержать значение (value) не меньшее, чем у каждого из 2 его потомков (если они есть). Пример: 76 / \ 45 34 / \ / 6 12 8 Как видим, здесь каждый родитель не ме- ньше потомка. Также видно,что дерево left- complete, поскольку последний ярус запол- няется слева направо,и все предыдущие пол- ностью заполнены.Так что перед нами типич- ный heap. Чтобы сгенерировать этот начальный heap, мы должны несколько раз использовать алгоритм под названием 'reheapification' (см. ниже). Предположим,что наш массив уже переведён в heap. Как разместить heap в памяти, не испо- льзуя указателей и прочей мути? А вот как. Просто-напросто в массив, ярус за ярусом: Array: 76 45 34 6 12 8 Position: 0 1 2 3 4 5 Обратите внимание, что при добавлении в массив новых элементов наш heap остаётся left-complete - последний ярус заполняется слева направо. Ещё обратите внимание, что дочерние узлы узла номер i находятся в ячейках i*2+1 и i*2+2. Однако встаёт воп- рос: как мы можем гарантировать, что value родителя не меньше, чем у его потомков- Возьмём тот же пример. Предположим, что мы добавили 102 в конец heap'а, то есть в ко- нец массива: 76 45 34 6 12 8 102 что соответствует следующему дереву: 76 / \ 45 34 / \ / \ 6 12 8 102 Это не heap. Чтобы сделать его heap'ом, попытаемся обменять 102 с его родителем (34). Получаем следующее дерево: 76 / \ 45 102 / \ / \ 6 12 8 34 Уже ближе, но всё равно не heap. Повто- ряем ту же операцию и получаем: 102 / \ 45 76 / \ / \ 6 12 8 34 Теперь это heap. Если вам интересно, то программа,реали- зующая вышеописанное, такова: /* Добавляет элемент 'value' в heap, лежащий в массиве 'array'.Надо следить, чтобы 'array' не переполнился */ void add_to_heap(int *array, int *size, int value) { int i = *size; /* Пишем value в конец массива, т.е. в первую пустую ячейку heap'а */ array[*size] = value; (*size)++; /* Пока массив не heap, двигаем value вверх */ while (i>0 && array[(i-1)/2]= len) \ break; \ else if ((right >= len)\ || (array[left] > array[right]))\ max = left; \ else \ max = right; \ if (array[k] > array[max]) \ break; \ SWAP(array[k], array[max]); \ k = max; \ } /* Sorts a list of ints using Heap Sort */ void sort_ints(int *array, int length) { int i, k; /* Строить heap из массива */ for (i = length / 2; i >= 0; i--) SORT_DOWN(i, length); for (i = length - 1; i > 0; i--) { /*Перенести корень в сортированную часть*/ SWAP(array[i], array[0]); /*Исправить heap в несортированной части*/ SORT_DOWN(0, i); } return; } ------------------------------------------ В книге "Структуры данных для персона- льных ЭВМ" (Й.Лэнгсам, М.Огенстайн, А.Те- ненбаум. - М.: "Мир", 1989) этот алгоритм называется "пирамидальной сортировкой". Авторы предлагают исходник на Бейсике. Об- ратите внимание на отличия от вышеприве- дённого сишного исходника: 1. Начальное дерево строится не снизу вверх, а сверху вниз (используя вышеизло- женный алгоритм add_to_heap, который в Си- версии не пригодился). Это требует вдвое больше проходов,и хотя сами проходы короче (их длины возрастают по мере заполнения heap'а), в среднем получается проигрыш. 2. В reheapification вместо цепочки swap'ов (4 обращения к массиву на каждом шаге) использована цепочка простых присва- иваний (2 обращения к массиву на каждом шаге), родительский узел предварительно запоминается и кладётся в массив в послед- нюю очередь. Это хороший выигрыш. 3. Имеется ряд лишних инкрементов, кото- рые можно оптимизировать. 4. Строки 5210-5250 по сути повторяют строки 5290-5320, только с меньшим числом вычислений - специально для корня. 5050 for k=2 to n 'у нас массив x(1..n) 5060 'вставить x(k) в существующую 'пирамиду размером k-1 5070 i=k 5080 y=x(k) 5090 j=int(i/2) 'j является отцом i 5100 if j<=0 then goto 5160 5110 if y<=x(j) then goto 5160 5120 x(i)=x(j) 5130 i=j 5140 j=int(i/2) 5150 goto 5100 5160 x(i)=y 5170 next k 5180 'мы удаляем x(1) и помещаем его в 'массиве в позицию, соответствующую 'его значению; затем перестраиваем 'пирамиду 5190 for k=n to 2 step -1 5200 y=x(k) 5210 x(k)=x(1) 5220 'переупорядочивание пирамиды 'порядка k-1; перемещение y в низ 'пирамиды на место, соответствующее 'его значению 5230 i=1 5240 j=2 5250 if (k-1>=3)and(x(3)>x(2)) then j=3 5260 'j является бОльшим сыном i в 'пирамиде размером k-1 5270 if j>k-1 then goto 5340 5280 if x(j)<=y then goto 5340 5290 x(i)=x(j) 5300 i=j 5310 j=2*j 5320 if j+1<=k-1 then if x(j+1)>x(j) then j=j+1 5330 goto 5260 5340 x(i)=y 5350 next k В следующий раз я, возможно, напишу о сортировке слиянием, которая,по моему мне- нию, наиболее удобна для сортировки имён файлов.