Inferno #09
31 июля 2006

For Coderz - О сортировке элементов массива.

       О сортировке
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]<array[i]) {
    int temp = array[i];
    array[i] = array[(i-1) / 2];
    array[(i-1) / 2] = temp;
    i = (i-1) / 2;
  }
 return;
} 

   Как  вы  видели, добавление 102 требует
лишь 2 итерации алгоритма. В худшем случае
требуется  log2(n) итераций, где n - число
элементов  дерева. То  есть вставка одного
числа в heap имеет сложность O(ln(n)) [эта
запись означает, что с ростом n число опе─
раций  асимптотически приближается к лога─
рифму n, умноженному  на какой-то постоян─
ный коэффициент]. Таким образом,добавление
n чисел  имеет сложность O(n*ln(n)). И это
для худшего случая!
   Вы,конечно,спросите: "Ну и как эта каб─
балистика  поможет  отсортировать массив?"
Объясню.
   Пусть мы имеем heap, который мы постро─
или  в предыдущем  примере. Он представлен
вот таким массивом:
 

           102 45 76 6 12 8 34

   Если  мы  возьмём  первый элемент этого
массива и обменяем его с последним, то по─
лучим:
 

           34 45 76 6 12 8 102

   Понятно, что это уже не heap! Но подож─
дите. Мы будем  теперь считать, что массив
стал  на  1  элемент короче (n-1 элементов
вместо  n). Точнее, разобьём массив на две
части. Первая будет деревом,а вторая - от─
сортированным массивом.(Будем шаг за шагом
уменьшать  первую  и  увеличивать вторую.)
Итак, мы имеем:
 

          34 45 76 6 12 8 - 102
( '-' будет разделять две эти части на на─ 
ших диаграммах,но на самом деле здесь один 
массив.) 

   Поскольку  мы  пока  не имеем heap, нам
следует провести 'reheapification'.Это тот
самый алгоритм, с помощью которого мы дол─
жны  были  построить  heap на первом этапе
(если не помните,отмотайте текст назад:-).
   Алгоритм reheapification очень простой.
Для конкретного родителя проверяем, нет ли
у  него  детей, больших  его. Если таковые
найдутся,обмениваем родителя с самым боль─
шим из них и смотрим, нет ли теперь у него
детей, больших его... и т.д. Похоже на то,
что мы делали,добавляя 102, но теперь све─
рху вниз,а не снизу вверх. Сложность такая
же - O(ln(n)). Подпрограмма, которая  про─
цитирована выше,нам не понадобится. Мы бу─
дем reheapifi'цировать только сверху вниз.
   Например,  пусть   нам  надо  выполнить
reheapification, начиная с корня (в данном
случае 34).
 

                     34
                   /    
                  45    76
                 /     /
               6  12  8

   Сравним  его с детьми: 45 и 76. Большее
дитя - 76, и оно больше родителя (34), по─
этому мы переставляем его с родителем. Те─
перь 76 - родитель, а 45 и 34 - дети.
 

                     76
                   /    
                  45    34
                 /     /
               6  12  8

   Далее  надо таким же образом проверить,
что  новоусыновлённый 34 не меньше его де─
тей - в  данном  случае одного ребёнка: 8.
Если всё в порядке,то конец алгоритма,ина─
че  опять  меняем  местами и т.д. В данном
случае всё в порядке, поэтому конец.

   Начальный heap мы строим, много раз по─
вторяя reheapification: начинаем с предпо─
следнего  яруса  (т.к. последний  не имеет
детей)  и кончаем первым (корнем). То есть
сначала  вызываем reheapification для узла
номер (int)(n/2),потом для (int)(n/2)-1, и
т.д. до 0.  В  конце  концов наверху будет
максимальное число, а все узлы,которые ле─
жат  ниже, будут  не меньше своих потомков
(ведь  мы  каждый из них reheapifi'цирова─
ли). Мы получили heap!

   Но мы отвлеклись, так и не отсортировав
наш массив.Мы попытались разбить массив на
отсортированую и  неотсортированную части,
потом  сделали  heap  из неотсортированной
части. Если мы опять повторим ту операцию,
то  есть вынем корневой элемент и поместим
его  в  отсортированную  часть массива, то
получим:
 

          8 45 34 6 12 - 76 102

   Теперь  у нас два отсортированных числа
- 76 и 102. Нанести, смыть, повторить, и в
конце концов получаем:
 

          - 6 8 12 34 45 76 102

   Массив отсортирован!

   Посмотрим, как  и  почему это работает.
Имея heap,мы знаем,что корень heap'а (пер─
вый, точнее  0-й, узел) носит максимальное
значение. По определению. Значит, мы можем
переместить  его  в  отсортированную часть
массива. Если мы после этого изъятия пере─
строим heap,то в новом heap'е корень будет
иметь новое максимальное значение. Другими
словами, второе  по величине значение мас─
сива  будет  в новом корне. Мы опять имеем
право перетащить 0-й элемент массива в от─
сортированную часть. Повторив это для всех
элементов неотсортированной части,мы полу─
чим полностью отсортированный массив!

   И насколько же хорош этот алгоритм сор─
тировки? Надо  провести  некоторый расчёт.
Первый этап нашего алгоритма - превращение
массива  в  heap. Для  этого мы перебираем
каждый узел,имеющий детей (таких узлов по─
рядка n/2), и проводим для него reheapifi─
cation. Поскольку в reheapification требу─
ется не больше ходов,чем есть ярусов в де─
реве (а их порядка log2(n) ), то всё прев─
ращение  в  heap  имеет  сложность порядка
O((n/2)*log2(n))=O(n*log2(n)) =O(n*ln(n)).
Далее, вторым этапом - переброска корней в
отсортированную  часть. Перестройка heap'а
после  изъятия корня требует в худшем слу─
чае  log2(n) ходов, а нам нужно делать эту
перестройку n-1 раз, итого выходит порядка
O(n*ln(n)) ходов.Поскольку первый этап то─
же имел сложность  O(n*ln(n)), то весь ал─
горитм имеет сложность O(n*ln(n)). А у со─
ртировки методом перебора-пузырька-поиска,
как мы её ни оптимизировали,была сложность
O(n*n), в ней число ходов с ростом n росло
куда быстрее!

   Я догадываюсь, что вам не терпится пос─
мотреть  исходник. Вот  он в том виде, как
использовался в игре Set Up Us The Bomb!!!
с небольшим  изменением  для сортировки по
возрастанию:

/* Macro to swap two numbers */ 
/* через XOR - для понта... */ 
 #define SWAP(a, b) {  
     (a) ^= (b); 
     (b) ^= (a); 
    (a) ^= (b); 
} 

/* Macro for reheapification */ 
 #define SORT_DOWN(pos,len) for (k=pos;;){ 
         int right, left, max; 
         left = 2 * k + 1;     
         right = left + 1;     
         if (left >= 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 

   В  следующий  раз я, возможно, напишу о
сортировке слиянием, которая,по моему мне─
нию, наиболее  удобна  для сортировки имён
файлов.




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Программистам - обмен опытом: программирование мультиколорных эффектов.
Стихи - "Выбор".
VTS - Модемы в Краснодаре.

В этот день...   22 сентября