Главная страница


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)
 
 

Вернуться к списку тем, сортированных по: возрастание даты  уменьшение даты  тема  автор 

 Тема:    Автор:    Дата:  
 S0rting FAQ 1/4   Ilia Kantor   19 Mar 2002 17:47:36 
Архивное /ru.algorithms/39463c976bba.html, оценка 2 из 5, голосов 10
Яндекс.Метрика
Valid HTML 4.01 Transitional