Быстрая сортировка

Материал из Википедии — свободной энциклопедии

Перейти к: навигация, поиск
Анимированная схема алгоритма
Анимированная схема алгоритма

Быстрая сортировка (англ. quicksort) — широко известный алгоритм сортировки, разработанный английским информатиком Чарльзом Хоаром. Самый быстрый из известных универсальных алгоритмов сортировки массивов (в среднем O(n log n) обменов при упорядочении n элементов).

Содержание

[править] Алгоритм

Быстрая сортировка использует стратегию «разделяй и властвуй». Шаги алгоритма таковы:

  1. Выбираем в массиве некоторый элемент, который будем называть опорным элементом. С точки зрения корректности алгоритма выбор опорного элемента безразличен. С точки зрения повышения эффективности алгоритма выбираться должна медиана, но без дополнительных сведений о сортируемых данных её обычно невозможно получить. Известные стратегии: выбирать постоянно один и тот же элемент, например, средний или последний по положению; выбирать элемент со случайно выбранным индексом.
  2. Операция разделения массива: реорганизуем массив таким образом, чтобы все элементы, меньшие или равные опорному элементу, оказались слева от него, а все элементы, большие опорного — справа от него. Обычный алгоритм операции:
    1. два индекса — l и r, приравниваются к минимальному и максимальному индексу разделяемого массива соответственно;
    2. вычисляется опорный элемент m;
    3. индекс l последовательно увеличивается до m или до тех пор, пока l-й элемент не превысит опорный;
    4. индекс r последовательно уменьшается до m или до тех пор, пока r-й элемент не окажется меньше опорного;
    5. если r = l — найдена середина массива — операция разделения закончена, оба индекса указывают на опорный элемент;
    6. если l < r — найденную пару элементов нужно обменять местами и продолжить операцию разделения с тех значений l и r, которые были достигнуты. Следует учесть, что если какая-либо граница (l или r) дошла до опорного элемента, то при обмене значение m изменяется на r или l соответственно.
  3. Рекурсивно упорядочиваем подмассивы, лежащие слева и справа от опорного элемента.
  4. Базой рекурсии являются наборы, состоящие из одного или двух элементов. Первый возвращается в исходном виде, во втором, при необходимости, сортировка сводится к перестановке двух элементов. Все такие отрезки уже упорядочены в процессе разделения.

Поскольку в каждой итерации (на каждом следующем уровне рекурсии) длина обрабатываемого отрезка массива уменьшается, по меньшей мере, на единицу, терминальная ветвь рекурсии будет достигнута всегда и обработка гарантированно завершится.

Интересно, что Хоар разработал этот метод применительно к машинному переводу: дело в том, что в то время словарь хранился на магнитной ленте, и если упорядочить все слова в тексте, их переводы можно получить за один прогон ленты.

[править] Оценка эффективности

QuickSort является существенно улучшенным вариантом алгоритма сортировки с помощью прямого обмена (его варианты известны как «Пузырьковая сортировка» и «Шейкерная сортировка»), известного, в том числе, своей низкой эффективностью. Принципиальное отличие состоит в том, что в первую очередь меняются местами наиболее удалённые друг от друга элементы массива. Любопытный факт: улучшение самого неэффективного прямого метода сортировки дало в результате самый эффективный улучшенный метод.

  • Лучший случай. Для этого алгоритма самый лучший случай — если в каждой итерации каждый из подмассивов делился бы на два равных по величине массива. В результате количество сравнений, делаемых быстрой сортировкой, было бы равно значению рекурсивного выражения CN = 2CN/2+N. Это дало бы наименьшее время сортировки.
  • Среднее. Даёт в среднем O(n log n) обменов при упорядочении n элементов. В реальности именно такая ситуация обычно имеет место при случайном порядке элементов и выборе опорного элемента из середины массива либо случайно.
    На практике быстрая сортировка значительно быстрее, чем другие алгоритмы с оценкой O(n log n), по причине того, что внутренний цикл алгоритма может быть эффективно реализован почти на любой архитектуре. 2CN/2 покрывает расходы по сортировке двух полученных подмассивов; N — это стоимость обработки каждого элемента, используя один или другой указатель. Известно также, что примерное значение этого выражения равно CN = N lg N.
  • Худший случай. Худшим случаем, очевидно, будет такой, при котором на каждом этапе массив будет разделяться на вырожденный подмассив из одного опорного элемента и на подмассив из всех остальных элементов. Такое может произойти, если в качестве опорного на каждом этапе будет выбран элемент либо наименьший, либо наибольший из всех обрабатываемых.
    Худший случай даёт O(n²) обменов, но количество обменов и, соответственно, время работы — это не самый большой его недостаток. Хуже то, что в таком случае глубина рекурсии при выполнении алгоритма достигнет n, что будет означать n-кратное сохранение адреса возврата и локальных переменных процедуры разделения массивов. Для больших значений n худший случай может привести к исчерпанию памяти во время работы алгоритма. Впрочем, на большинстве реальных данных можно найти решения, которые минимизируют вероятность того, что понадобится квадратичное время.

[править] Улучшения

  • При выборе опорного элемента из данного диапазона случайным образом худший случай становится очень маловероятным и ожидаемое время выполнения алгоритма сортировки — O(n log n).
  • Во избежание достижения опасной глубины рекурсии в худшем случае (или при приближении к нему) возможна модификация алгоритма, устраняющая одну ветвь рекурсии: вместо того, чтобы после разделения массива вызывать рекурсивно процедуру разделения для обоих найденных подмассивов, рекурсивный вызов делается только для меньшего подмассива, а больший обрабатывается в цикле в пределах этого же вызова процедуры. С точки зрения эффективности в среднем случае разницы практически нет: накладные расходы на дополнительный рекурсивный вызов и на организацию сравнения длин подмассивов и цикла — примерно одного порядка. Зато глубина рекурсии ни при каких обстоятельствах не превысит lg2n, а в худшем случае она вообще будет не более 2 — вся обработка пройдёт в цикле первого уровня рекурсии.
  • Использовать в качестве опорного элемента медиану-средний элемент массива. Т.к. медиана ищется за O(n), алгоритм всегда будет работать за O(n log n).

[править] Достоинства и недостатки

Достоинства:

  • Самый быстродействующий из всех существующих алгоритмов обменной сортировки — быстрее него только специализированные алгоритмы, использующие специфику сортируемых данных.
  • Простая реализация.
  • Хорошо сочетается с алгоритмами кэширования и подкачки памяти.
  • Хорошо работает на «почти отсортированных» данных — если исходный массив уже близок к отсортированному, алгоритм не приводит к излишним перестановкам уже стоящих в правильном порядке элементов.

Недостатки:

  • При классической реализации требует в худшем случае много дополнительной памяти. Правда, вероятность возникновения худшего случая ничтожна.
  • Неустойчив — если требуется устойчивость, приходится расширять ключ.

[править] Примеры реализации

Представленные здесь реализации, кроме варианта на языке Паскаль, используют в качестве опорного элемента один из крайних элементов подмассива. Все эти реализации страдают одним общим недостатком: при передаче им уже отсортированного массива в качестве параметра они приводят к самому худшему случаю. Реализация на Паскале выбирает в качестве опорного средний элемент, в результате отсортированный массив становится лучшим случаем - он сортируется за наименьшее время, потому что каждый раз опорным элементом оказывается медиана.

[править] C

Первый вариант Работает для любых чисел

   int partition (mytype * m, int a, int b) 
    {
      int i = a;
      for (int j = a; j <= b; j++)    // просматриваем с a по b
       {
         if (m[j] <= m[b])            // если элемент m[j] не превосходит m[b],
          {
            swap(m[i], m[j]);         // меняем местами m[j] и m[a], m[a+1], m[a+2] и так далее...
                                      // то есть переносим элементы меньшие m[b] в начало,
                                      // а затем и сам m[b] «сверху»
            i++;                      // таким образом последний обмен: m[b] и m[i], после чего i++
          }
       }
      return i-1;                     // в индексе i хранится <новая позиция элемента m[b]> + 1
    }
 
   void quicksort (mytype * m, int a, int b) // a - начало подмножества, b - конец
    {                                        // для первого вызова: a = 0, b = <элементов в массиве> - 1
     if (a >= b) return;
     int c = partition (m, a, b);
     quicksort (m, a, c-1);
     quicksort (m, c+1, b);
    }

Второй вариант. Работает для любых чисел

void qsrt (mytype &v, int l, int r)
/*
  рекурсивная подпрограмма быстрой сортировки
  Входные параметры
  &v - ссылка на сортируемый массив
  (Поправка (3.06.2008: если сортируемый массив объявлен статически, то необходим не параметр 
   int &v, а int (&v)[N], где N длина массива.))
  И просьба к автору, приводить пример функции, откуда осуществляется вызов данной функции.
  l, r - левая и правая граница сортируемой области
*/
{
  if (l>=r) return;               // Условие выхода из рекурсии (достигнута база рекурсии)
 
  int l1, r1, m;
  mytype tmp;
  l1 = l; r1 = r;                 // Запомнить начальное значение границ
  m = (r+l)/2;                    // вычисление опорного элемента
 
  while(l<r){                     // Итеративное разделение массива
    while ((v[l]<=v[m])&&(l<m)) l++;     // сдвиг левой границы
    while (v[r]>v[m]) r--;               // сдвиг правой границы
 
    tmp = v[l];                   // обмен граничных элементов
    v[l] = v[r];
    v[r] = tmp;
    if (l == m) m = r;            // обновление индекса опорного элемента в случае
    else if (r == m) m = l;       //  если опорный элемент участвовал в обмене
  }
  qsrt(v, l1, m-1);               // рекурсивная сортировка полученных массивов
  qsrt(v, m+1, r1);
}

Третий вариант. (шаблон на C++)

// A - сортируемый массив, a и b - границы
template <class T>
void Qsort(T *A, int a, int b){
    int i, j, m;
    T c;
    if (a>=b) return;                                  
    for (i=a, j=b, m=1; i<j; m>0?j--:i++){
        if (A[i]>A[j]){  
            c = A[i]; 
            A[i] = A[j]; 
            A[j] = c;
            m = -m;}}
    Qsort(A, a, i-1); 
    Qsort(A, i+1, b);
    return;
}
/* Данная шаблонная функция упорядочивает массив данных любого типа, для которого определён оператор "больше". */
 
// пример вызова
#include <math.h>
 
int main(){
    double A[10];
    int B[10];
    for (int i=0; i<10; i++){   // Заполнение массивов
        A[i] = sin(i);         // Псевдослучайными числами
        B[i] = int(sin(i)*10);}
    Qsort(A, 0, 9);            // Сортировка вещественных чисел
    Qsort(B, 0, 9);            // Сортировка целых чисел
}

[править] C#

   int partition (int[] m, int a, int b) 
    {
      int i = a;
      for (int j = a; j <= b; j++)    // просматриваем с a по b
       {
         if (m[j] <= m[b])               // если элемент a[j] не превосходит a[b],
          {
            int t = m[i];                    // меняем местами a[j] и a[a], a[a+1], a[a+2] и так далее...
            m[i] = m[j];                 // то есть переносим элементы меньшие a[b] в начало,
            m[j] = t;                    // а затем и сам a[b] «сверху»
            i++;                         // таким образом последний обмен: a[b] и a[i], после чего i++
          }
       }
      return i-1;                        // в индексе i хранится <новая позиция элемента a[b]> + 1
    }
 
   void quicksort (int[] m, int a, int b) // a - начало подмножества, b - конец
    {                                     // для первого вызова: a = 0, b = <элементов в массиве> - 1
     if (a >= b) return;
     int c = partition (m, a, b);
     quicksort (m, a, c-1);
     quicksort (m, c+1, b);
    }


[править] Ruby

Вариант 1:

class Array 
   def qsort
      return self.dup if size <=1
      l,r = partition   {|x| x <= self.first}
      c,l = l.partition {|x| x == self.first}
      l.qsort + с + r.qsort 
   end
end

Вариант 2:

class Array 
   def partition3
      a = Array.new(3) {|i| []}
      each do |x|
         a[yield(x)].push x
      end
      a
   end
   def qsort
      return self.dup if size <=1
      c,l,r = partition3 {|x| first <=> x}
      l.qsort + c +  r.qsort
   end
end

[править] Python

 def qsort(L):
    if L == []: return []
    return qsort([x for x in L[1:] if x< L[0]]) + L[0:1] + qsort([x for x in L[1:] if x>=L[0]])


[править] Haskell

 qsort []     = []
 qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)

[править] Pascal

В данном примере показан наиболее полный вид алгоритма, очищенный от особенностей, обусловленных применяемым языком. В комментариях показано несколько вариантов. Представленный вариант алгоритма выбирает опорный элемент псевдослучайным образом, что, теоретически, сводит вероятность возникновения самого худшего или приближающегося к нему случая к минимуму. Недостаток его — зависимость скорости алгоритма от реализации генератора псевдослучайных чисел. Если генератор работает медленно или выдаёт плохие последовательности ПСЧ, возможно замедление работы. В комментарии приведён вариант выбора среднего значения в массиве — он проще и быстрее, хотя, теоретически, может быть хуже.

Внутреннее условие, помеченное комментарием «это условие можно убрать» — необязательно. Его наличие влияет на действия в ситуации, когда поиск находит два равных ключа: при наличии проверки они останутся на местах, а при отсутствии — будут обменены местами. Что займёт больше времени — проверки или лишние перестановки, — зависит как от архитектуры, так и от содержимого массива (очевидно, что при наличии большого количества равных элементов лишних перестановок станет больше). Следует особо отметить, что наличие условия не делает данный метод сортировки устойчивым.

const max=20; { можно и больше... }
type
  list = array[1..max] of integer;
 
procedure quicksort(var a: list; Lo,Hi: integer);
 
  procedure sort(l,r: integer);
  var
    i,j,x,y: integer;
  begin
    i:=l; j:=r; x:=a[random(r-l+1)+l]; { x := a[(r+l) div 2]; - для выбора среднего элемента } 
    repeat
      while a[i]<x do i:=i+1; { a[i] > x  - сортировка по убыванию}
      while x<a[j] do j:=j-1; { x > a[j]  - сортировка по убыванию}
      if i<=j then begin
        if a[i] > a[j] then begin  { это условие можно убрать } {a[i] < a[j] при сортировке по убыванию}
          y:=a[i]; a[i]:=a[j]; a[j]:=y; 
        end; 
        i:=i+1; j:=j-1;
      end;
    until i>j;
    if l<j then sort(l,j);
    if i<r then sort(i,r);
  end; {sort}
 
begin {quicksort};
  randomize; { нужно только если используется выборка случайного опорного элемента }
  sort(Lo,Hi);
end; {quicksort}

Быстрая сортировка, нерекурсивный вариант Нерекурсивная реализация "Быстрой сортировки" реализованной через "стэк" (односторонний список). Функции compare и change реализуются в зависимости от типа данных.

procedure quickSort(var X: itemArray; n: integer);
type p_node = ^node;
     node   = record node: integer; next: p_node; end;
var l,r,i,j: integer;
      stack: p_node;
       temp: item;
 
  procedure push(i: integer);
  var temp: p_node;
   begin
    new(temp);
    temp^.node:=i;
    temp^.next:=stack;
    stack:=temp;
   end;
 
  function pop: integer;
  var temp: p_node;
   begin
    if stack=nil then
      pop:=0
    else
     begin
      temp:=stack;
      pop:=stack^.node;
      stack:=stack^.next;
      dispose(temp);
     end;
   end;
 
 begin
  stack:=nil;
  push(n-1);
  push(0);
  repeat
    l:=pop;
    r:=pop;
    if r-l=1 then
     begin
      if compare(X[l],X[r]) then
        change(X[l],X[r])
     end
    else
     begin
      temp:=x[(l+r) div 2]; {random(r-l+1)+l}
      i:=l;
      j:=r;
      repeat
        while compare(temp,X[i]) do i:=i+1;
        while compare(X[j],temp) do j:=j-1;
        if i<=j then
         begin
          change(X[i],X[j]);
          i:=i+1;
          j:=j-1;
         end;
      until i>j;
      if l<j then
       begin
        push(j);
        push(l);
       end;
      if i<r then
       begin
        push(r);
        push(i);
       end;
     end;
  until stack=nil;
 end;

[править] Prolog

split(H, [A|X], [A|Y], Z) :-
  order(A, H), split(H, X, Y, Z).
split(H, [A|X], Y, [A|Z]) :-
  not(order(A, H)), split(H, X, Y, Z).
split(_, [], [], []).

quicksort([], X, X).

quicksort([H|T], S, X) :-
  split(H, T, A, B),
  quicksort(A, S, [H|Y]),
  quicksort(B, Y, X).



[править] Perl

 #! /usr/bin/perl
 
 use strict;
 
 sub qsort {
     my ($q, $s, $e) = @_;
     my $m = $s-1;
     
     for (my $i = $s; $i < $e; $i++) {
         if ($q->[$i] < $q->[$e]) {
             $m++;
             ($q->[$m], $q->[$i]) = ($q->[$i], $q->[$m]);
         }
     }
     
     $m++;
     ($q->[$m], $q->[$e]) = ($q->[$e], $q->[$m]);
     qsort($q, $s, $m-1) if $s < $m-1;
     qsort($q, $m+1, $e) if $m+1 < $e;
 }
 
 my @data = map { int(rand(10)) } (1 .. 30);
 
 print "@data\n";
 qsort(\@data, 0, $#data);
 print "@data\n";

[править] Литература

  • Ананий В. Левитин Глава 4. Метод декомпозиции: Быстрая сортировка // Алгоритмы: введение в разработку и анализ = Introduction to The Design and Analysis of Aigorithms. — М.: «Вильямс», 2006. — С. 174-179. — ISBN 0-201-74395-7
  • Томас Х. Кормен и др. Глава 7. Быстрая сортировка // Алгоритмы: построение и анализ = INTRODUCTION TO ALGORITHMS. — 2-е изд. — М.: «Вильямс», 2006. — С. 1296. — ISBN 0-07-013151-1