:: алгоритмы  и методы :: :: олимпиадные задачи :: :: связь :: :: о сайте ::
Путь: Сортировка » FAQ » Вопрос 11
  Вопрос 11




===========================================================================
> 11. Что такое Байтовая, Цифровая, Радиксная или Распределяющая сортировка ?
===========================================================================
A: Kantor Ilia
  
  Пусть имеем максимум по k байт в каждом ключе ( хотя за элемент
 сортировки вполне можно принять и что-либо другое, например слово -
 двойной байт, или буквы, если сортируются строки). k должно быть
 известно заранее, до сортировки.

 Разрядность данных ( количество возможных значений элементов ) - m,
также должна быть известна заранее и постоянна.
                                                               
  Если мы сортируем слова, то элемент сортировки - буква. m = 33.
 Если в самом длинном слове 10 букв, k = 10.

 Обычно мы будем сортировать данные по ключам из k байт, m=256.

----------------------------------------------------------------------
  Пусть у нас есть массив source из n элементов по одному байту в каждом.

 Для примера можете выписать на листочек массив 
 source[1..7] = { 7,9,8,5,4,7,7 }, и проделать с ним все операции,
 имея в виду m=9. n здесь, очевидно, равно 7.
                                                            
     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. 
     Hапример, 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(kn) шагов. Если количество
возможных различных ключей ненамного превышает общее их число, то
поразрядная сортировка оказывается гораздо быстрее даже 'быстрой
сортировки' !

     Распределяющая сортировка, требует O(n) дополнительной памяти.

 Пример(учебный) реализации radixsort на Си++ для long int'ов.
 
 #include <iostream.h>
 #include <stdlib.h>
 #include <string.h>

 void radix (int byte, long N, long *source, long *dest)
 {
  // *source - входной массив,
  // *dest - отсортированный.
  long count[256];    // вообще говоря, можно обойтись 
  long index[256];    // одним массивом count[]
  
  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)
 {
  // Сортируем по всем 4 разрядам
  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';
 }
 
 Примечания.
 
1.  К сожалению, или к счастью, единица информации в современной 
 технике способна принимать лишь 2 значения: 1 и 0.
  А значит, любые компьютерные данные тоже могут принимать 
 ограниченное количество значений, так как состоят из некоторого 
 числа битов ;-)). 
  Таким образом, распределяющая сортировка потенциально применима
 где угодно. Другое дело, что если разрядность данных большая 
 по сравнению с общим количеством элементов, то скорость работы
 оставит желать много лучшего.

2. Алгоритм, описанный выше, очевидно, требует O(n) памяти.
Существует похожий алгоритм, использующий лишь O(logn).
  Модифицированная Radix Sort:
Будем сортировать по каждой позиции символа 'на месте', начиная справа:
сначала сортируем по первой позиции, затем к каждой части массива,
относящейся к одному и тому же значению, рекурсивно 
применяем ту же процедуру и т.д.

 Этот вариант называется 'Radix exchange sort' и 
представляет собой скорее разновидность QuickSort. 
 Hа мой взгляд, она объединяет больше плохих черт обоих методов,
нежели хороших, но вполне может пригодиться в какой-то ситуации.

>  Существует вариант распределяющей сортировки для списков,
> каждый элемент которых принимает конечное множество значений.
> Он даже проще в реализации, понимании и использовании.

Предположим, что элементы линейного списка В есть Т-разрядные
положительные десятичные числа D(j,n) - j-я справа цифра в десятичном 
числе n>=0, т.е. D(j,n)=floor(n/m)%10, где m=10^(j-1).
 (floor - округление в меньшую сторону)
 Пусть В0,В1,...,В9 - вспомогательные списки (карманы), вначале пустые. 
Для реализации распределяющей сортировки выполняется процедура, 
состоящая из двух процессов, называемых распределение и сборка 
для j=1,2,...,T. 

PАСПРЕДЕЛЕHИЕ 
заключается в том, что элемент Кi (i=1,N) из В добавляется как 
последний в список Bm, где m=D(j,Ki), и таким образом получаем 
десять списков, в каждом из которых j-тые разряды чисел одинаковы и 
равны m. 

СБОРКА 
объединяет списки В0,В1,...,В9 в этом же порядке, 
образуя один список В.
 
> Число карманов - количество возможных значений элемента списка.

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

В представленной ниже программе функция pocket реализует распределяющую 
сортировку связанного линейного списка (указатель q), в котором содержатся
Т-разрядные десятичные положительные числа, без использования 
дополнительной памяти;
в функции a[i], b[i] указывают соответственно на первый и на последний 
элементы кармана Bi. 

/*  распределяющая  сортировка  списка    */
#include <stdlib.h>
#include <stdio.h>
	 
typedef struct SP1_ { 
   long val;
   struct SP1_ *next; 
} SP1;

 /*    функция сортировки возвращает указатель на начало
        отсортированного списка      */
SP1 *pocket(SP1 *q,int t) {
    /* t - разрядность (максимальная длина числа) */
   int i,j,k,m=1;
   SP1 *r, *gg, *a[10], *b[10];
   gg=q;
   for(j=1;j<=t;j++) { 
       for(i=0;i<=9;i++) a[i]=(b[i]=NULL);
       while(q!=NULL) {
           k=((int)(q->val/m))%(int)10;
           r=b[k];
           if (a[k]==NULL) a[k]=q; else r->next=q;
           r=b[k]=q;
           q=q->next;
           r->next=NULL;
       }
       for(i=0;i<=9;i++) if (a[i]!=NULL) break;
       q=a[i];
       r=b[i];
       for(k=i+1;k<=9;k++)
           if(a[k]!=NULL) { 
	       r->next=a[k];
               r=b[k];
           }
       m=m*10;
       }
   return (gg);
}

   /* Тестовая программа */	 
void main() {
   int i;
   SP1 *q, *r;
   /* Это будем сортировать */
   long a[14]={ 0,7,8,3,52,14,16,18,15,13,42,30,35,26 };

   q=(SP1 *)malloc(sizeof(SP1));
   q->val=a[0];
   r=q;
   for(i=1;i<14;i++) {     /*  формирование списка  */
         r->next=(SP1 *)malloc(sizeof(SP1));
         r->next->val=a[i];
         r=r->next;
   }
   r->next = NULL;

   /* Список сформирован, q указывает на начало */
    
   r=pocket(q,2);  /* Запускаем сортировку */
   printf("\nresult:\n");          /*  печать результатов   */
   while (r!=NULL) {
      printf(" %d",r->val);
      r=r->next;
   }
}