Быстрая сортировка: выбор точки поворота


109

При внедрении быстрой сортировки вам необходимо выбрать точку поворота. Но когда я смотрю на псевдокод, подобный приведенному ниже, неясно, как мне выбрать точку опоры. Первый элемент списка? Что-то другое?

 function quicksort(array)
     var list less, greater
     if length(array) ≤ 1  
         return array  
     select and remove a pivot value pivot from array
     for each x in array
         if x ≤ pivot then append x to less
         else append x to greater
     return concatenate(quicksort(less), pivot, quicksort(greater))

Может ли кто-нибудь помочь мне понять концепцию выбора точки разворота и понять, требуют ли разные сценарии разные стратегии.


Ответы:


87

Выбор случайной оси сводит к минимуму вероятность того, что вы столкнетесь с наихудшей производительностью O (n 2 ) (всегда выбор первого или последнего приведет к наихудшей производительности для почти отсортированных или почти обратносортированных данных). Выбор среднего элемента также будет приемлемым в большинстве случаев.

Кроме того, если вы реализуете это самостоятельно, существуют версии алгоритма, которые работают на месте (то есть без создания двух новых списков с последующим их объединением).


10
Я бы поддержал идею о том, что самостоятельное выполнение поиска может не стоить усилий. Кроме того, будьте осторожны при выборе случайных чисел, поскольку генераторы случайных чисел иногда работают медленно.
PeterAllenWebb

@Jonathan Leffler ответ лучше
Натан

60

Это зависит от ваших требований. Выбор опоры в случайных делает его труднее создать набор данных, который генерирует производительность O (N ^ 2). «Медиана из трех» (первый, последний, средний) - это тоже способ избежать проблем. Однако остерегайтесь относительной производительности сравнений; если ваши сравнения дорогостоящие, то Mo3 выполняет больше сравнений, чем выбор (единственное значение поворота) наугад. Сравнение записей базы данных может быть дорогостоящим.


Обновление: добавление комментариев в ответ.

mdkess утверждал:

«Медиана 3» НЕ является первой последней серединой. Выберите три случайных индекса и возьмите их среднее значение. Все дело в том, чтобы убедиться, что ваш выбор опорных точек не является детерминированным - если это так, данные наихудшего случая могут быть легко сгенерированы.

На что я ответил:

  • Анализ алгоритма поиска Хоара с разделением медианы трех (1997), авторы П. Киршенхофер, Х. Продингер, К. Мартинес поддерживают ваше утверждение (что «медиана трех» - это три случайных элемента).

  • На сайте portal.acm.org описана статья Ханну Эркио «Перестановка наихудшего случая для быстрой сортировки медианы трех», опубликованная в The Computer Journal, том 27, № 3, 1984 г. [Обновление 2012-02- 26: Получил текст для статьи . Раздел 2 «Алгоритм» начинается со слов: « Используя медиану первого, среднего и последнего элементов A [L: R], в большинстве практических ситуаций можно добиться эффективного разбиения на части довольно равных размеров. «Таким образом, обсуждается подход Mo3« первый-средний-последний ».]

  • Еще одна интересная короткая статья доктора медицины Макилроя «Противник-убийца для быстрой сортировки» , опубликованная в Software-Practice and Experience, Vol. 29 (0), 1–4 (0 1999). Он объясняет, как заставить почти любую Quicksort вести себя квадратично.

  • Технический журнал AT&T Bell Labs, октябрь 1984 г. «Теория и практика построения рабочей процедуры сортировки» заявляет: «Хоар предложил разбить несколько случайно выбранных линий вокруг медианы. [...] Седжвик рекомендовал выбрать медианное значение первых [. ..] последний [...] и средний ». Это указывает на то, что в литературе известны оба метода определения «медианы из трех». (Обновление 2014-11-23: статья, похоже, доступна в IEEE Xplore или в Wiley - если у вас есть членство или вы готовы заплатить взнос.)

  • «Разработка функции сортировки» Дж. Л. Бентли и М. Д. Макилроя, опубликованная в журнале Software Practice and Experience, Vol 23 (11), ноябрь 1993 г., подробно обсуждает проблемы, и они выбрали алгоритм адаптивного разделения, частично основанный на размер набора данных. Много обсуждают компромиссы для различных подходов.

  • Поиск в Google по запросу «среднее из трех» отлично подходит для дальнейшего отслеживания.

Спасибо за информацию; Раньше я сталкивался только с детерминированной «медианой из трех».


4
Медиана 3 НЕ является первой последней серединой. Выберите три случайных индекса и возьмите их среднее значение. Все дело в том, чтобы убедиться, что ваш выбор опорных точек не является детерминированным - если это так, данные наихудшего случая могут быть легко сгенерированы.
mindvirus

Я читал abt introsort, который сочетает в себе хорошие функции быстрой и heapsort. Подход к выбору точки поворота с использованием медианы трех не всегда может быть благоприятным.
Сумит Кумар Саха

4
Проблема с выбором случайных индексов в том, что генераторы случайных чисел довольно дороги. Хотя это не увеличивает большие затраты на сортировку, это, вероятно, замедлит работу, чем если бы вы просто выбрали первый, последний и средний элементы. (В реальном мире, держу пари, никто не создает надуманных ситуаций, чтобы замедлить вашу быструю сортировку.)
Кевин Чен

20

Хех, я только что вел этот класс.

Есть несколько вариантов.
Просто: выберите первый или последний элемент диапазона. (плохо для частично отсортированного ввода) Лучше: выберите элемент в середине диапазона. (лучше для частично отсортированного ввода)

Однако выбор любого произвольного элемента рискует плохо разбить массив размера n на два массива размером 1 и n-1. Если вы делаете это достаточно часто, ваша быстрая сортировка рискует стать O (n ^ 2).

Одно улучшение, которое я заметил, - это выбор медианы (первая, последняя, ​​средняя); В худшем случае он все еще может перейти к O (n ^ 2), но, вероятно, это редкий случай.

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

Если вы все еще сталкиваетесь с проблемами, выбирайте средний путь.


1
Мы провели эксперимент в нашем классе, получив k наименьших элементов из массива в отсортированном порядке. Мы сгенерировали случайные массивы, затем использовали либо минимальную кучу, либо рандомизированный выбор и быструю сортировку с фиксированной сводной точкой и подсчитали количество сравнений. На этих «случайных» данных второе решение работало в среднем хуже, чем первое. Переход на рандомизированный пивот решает проблему производительности. Таким образом, даже для предположительно случайных данных фиксированный поворот работает значительно хуже, чем рандомизированный.
Роберт С. Барнс,

Почему при разделении массива размера n на два массива размером 1 и n-1 существует риск стать O (n ^ 2)?
Аарон Франке

Предположим, что массив имеет размер N. Разделение на размеры [1, N-1]. Следующим шагом является разделение правой половины на [1, N-2]. и так далее, пока у нас не будет N разделов размером 1. Но если бы мы разделили пополам, мы бы делали 2 раздела по N / 2 на каждом шаге, что привело бы к члену сложности Log (n);
Крис Кадмор

11

Никогда не выбирайте фиксированный поворот - это может быть атаковано, чтобы использовать время выполнения вашего алгоритма O (n ^ 2) в худшем случае, которое просто напрашивается на проблемы. Наихудший случай выполнения Quicksort возникает, когда в результате разбиения получается один массив из 1 элемента и один массив из n-1 элементов. Предположим, вы выбрали первый элемент в качестве раздела. Если кто-то скармливает вашему алгоритму массив, который находится в порядке убывания, ваша первая точка поворота будет самой большой, поэтому все остальное в массиве переместится влево от него. Затем, когда вы выполняете рекурсию, первый элемент снова будет самым большим, поэтому вы еще раз поместите все слева от него и так далее.

Лучшим методом является метод медианы из трех, при котором вы выбираете три элемента наугад и выбираете середину. Вы знаете, что выбранный вами элемент не будет первым или последним, но также, согласно центральной предельной теореме, распределение среднего элемента будет нормальным, что означает, что вы будете стремиться к среднему (и, следовательно, , n lg n время).

Если вы абсолютно хотите гарантировать время выполнения алгоритма O (nlgn), метод столбцов из 5 для поиска медианы массива выполняется за время O (n), что означает, что рекуррентное уравнение для быстрой сортировки в худшем случае будет быть T (n) = O (n) (найти медиану) + O (n) (разбиение) + 2T (n / 2) (рекурсивно влево и вправо). По основной теореме это O (n lg n) . Однако постоянный коэффициент будет огромным, и если производительность в худшем случае является вашей основной задачей, используйте вместо этого сортировку слиянием, которая в среднем лишь немного медленнее, чем быстрая сортировка, и гарантирует время O (nlgn) (и будет намного быстрее чем эта хромая медианная быстрая сортировка).

Объяснение алгоритма медианы медиан


6

Не пытайтесь стать слишком умным и комбинировать стратегии поворота. Если в сочетании медианы 3 со случайным шарниром, выбирая медианы первого, последнего и случайного индекс в середине, то вы все равно будете уязвимы для многих распределений, которые посылают медиану 3 квадратичных (так его на самом деле хуже, чем простой случайный поворот)

Например, при распределении органа трубы (1,2,3 ... N / 2..3,2,1) первый и последний оба будут равны 1, а случайный индекс будет некоторым числом больше 1, взятие медианы дает 1 ( либо первое, либо последнее), и вы получите крайне несбалансированное разбиение.


2

Так проще разбить быструю сортировку на три части.

  1. Функция обмена или замены элемента данных
  2. Статистическая сумма
  3. Обработка перегородок

Это ненамного неэффективнее, чем одна длинная функция, но ее намного легче понять.

Код следующий:

/* This selects what the data type in the array to be sorted is */

#define DATATYPE long

/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */

void swap (DATATYPE *x, DATATYPE *y){  
  DATATYPE Temp;

  Temp = *x;        // Hold current x value
  *x = *y;          // Transfer y to x
  *y = Temp;        // Set y to the held old x value
};


/* This is the partition code */

int partition (DATATYPE list[], int l, int h){

  int i;
  int p;          // pivot element index
  int firsthigh;  // divider position for pivot element

  // Random pivot example shown for median   p = (l+h)/2 would be used
  p = l + (short)(rand() % (int)(h - l + 1)); // Random partition point

  swap(&list[p], &list[h]);                   // Swap the values
  firsthigh = l;                                  // Hold first high value
  for (i = l; i < h; i++)
    if(list[i] < list[h]) {                 // Value at i is less than h
      swap(&list[i], &list[firsthigh]);   // So swap the value
      firsthigh++;                        // Incement first high
    }
  swap(&list[h], &list[firsthigh]);           // Swap h and first high values
  return(firsthigh);                          // Return first high
};



/* Finally the body sort */

void quicksort(DATATYPE list[], int l, int h){

  int p;                                      // index of partition 
  if ((h - l) > 0) {
    p = partition(list, l, h);              // Partition list 
    quicksort(list, l, p - 1);        // Sort lower partion
    quicksort(list, p + 1, h);              // Sort upper partition
  };
};

1

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


1

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

Если вы сортируете что-то только с линейным доступом (например, связанный список), то лучше выбрать первый элемент, потому что это самый быстрый элемент для доступа. Однако здесь, если список уже отсортирован, вы облажались - один раздел всегда будет нулевым, а другой - всем, что приведет к худшему времени.

Однако для связанного списка выбор чего-либо, кроме первого, только усугубит ситуацию. Он выбирает средний элемент в списке-списке, вам нужно будет проходить его на каждом шаге раздела - добавляя операцию O (N / 2), которая выполняется logN раз, что составляет общее время O (1,5 N * log N) и это если мы знаем, сколько длится список, прежде чем мы начнем - обычно мы этого не делаем, поэтому нам пришлось бы пройти весь путь, чтобы пересчитать их, затем пройти половину пути, чтобы найти середину, затем пройти через третий раз, чтобы сделать фактический раздел: O (2.5N * log N)


0

В идеале точка поворота должна быть средним значением во всем массиве. Это снизит шансы получить худшую производительность.


1
телега впереди лошади здесь.
ncmathsadist

0

Сложность быстрой сортировки сильно зависит от выбора значения сводной таблицы. например, если вы всегда выбираете первый элемент в качестве стержня, сложность алгоритма становится такой же худшей, как O (n ^ 2). вот умный метод выбора сводного элемента: 1. выберите первый, средний и последний элемент массива. 2. Сравните эти три числа и найдите число, которое больше одного и меньше другого, т.е. медиана. 3. Сделайте этот элемент поворотным.

выбирая стержень с помощью этого метода разбивает массив почти две половины и, следовательно, сложность сводится к O (Nlog (п)).


0

В среднем, медиана 3 подходит для малых n. Медиана 5 немного лучше для большего n. Девятое, которое представляет собой «среднее из трех медиан из трех», даже лучше для очень больших n.

Чем выше вы поднимаете выборку, тем лучше вы получаете при увеличении n, но улучшение резко замедляется при увеличении выборки. И вы несете накладные расходы на отбор и сортировку образцов.


0

Я рекомендую использовать средний индекс, так как он легко рассчитывается.

Вы можете рассчитать его округлением (array.length / 2).


-1

В действительно оптимизированной реализации метод выбора точки поворота должен зависеть от размера массива - для большого массива стоит потратить больше времени на выбор хорошей точки поворота. Не проводя полного анализа, я бы предположил, что «середина из O (log (n)) элементов» - хорошее начало, и это дает дополнительный бонус в виде отсутствия дополнительной памяти: использование хвостового вызова на большем разделе и в- При разделении на разделы мы используем одну и ту же дополнительную память O (log (n)) почти на каждом этапе алгоритма.


1
Найти середину из 3 элементов можно за постоянное время. Еще больше, и мы, по сути, должны отсортировать подмассив. Когда n становится большим, мы снова возвращаемся к проблеме сортировки.
Крис Кадмор
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.