Quicksort: вибір вивороту


109

При впровадженні Quicksort однією з речей, що вам потрібно зробити, є вибір стрижня. Але коли я дивлюсь на псевдокод, як той, що нижче, не зрозуміло, як я повинен вибрати шарнір. Перший елемент списку? Щось ще?

 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 відповідь краща
Nathan

60

Це залежить від ваших вимог. Вибір випадкового стрижня ускладнює створення набору даних, що генерує продуктивність O (N ^ 2). "Середня серед трьох" (перша, остання, середина) - це також спосіб уникнути проблем. Однак остерігайтеся відносної ефективності порівнянь; якщо ваші порівняння коштують дорого, то Mo3 робить більше порівнянь, ніж вибір (одне значення зсуву) навмання. Порівняння записів баз даних може бути дорогим для порівняння.


Оновлення: притягування коментарів до відповіді.

mdkess стверджує:

"Медіана 3" НЕ є першою останньою серединою. Виберіть три випадкові індекси та візьміть середнє значення цього. Вся справа в тому, щоб переконатися, що ваш вибір стрижнів не є детермінованим - якщо це так, найгірші дані можуть бути досить генеровані.

На що я відповів:

  • Аналіз алгоритму знаходження Хоара за розділом "Медіана-три" (1997) P Кіршенхофер, H Prodinger, C Martínez підтримує ваше твердження (що "медіана з трьох" - це три випадкові предмети).

  • На порталі portal.acm.org є стаття, що розповідається про "Найгіршу перестановку випадку середнього трьох кваксортів" Ханни Еркіо, опубліковану в "Комп'ютерному журналі", т. 27, № 3, 1984 р. [Оновлення 2012-02- 26: Отримав текст статті . Розділ 2 "Алгоритм" починається: " За допомогою медіани першого, середнього та останнього елементів A [L: R] можна досягти ефективних розділів на частини досить рівних розмірів у більшості практичних ситуацій. "Таким чином, він обговорює підхід Mo3 першого-середнього-останнього.]

  • Інша цікава стаття - доктор Мак-Ілрой, "Вбивця противника Quicksort" , опублікований в Software-Practice and Experience, Vol. 29 (0), 1–4 (0 1999). Це пояснює, як змусити практично будь-який Quicksort вести себе квадратично.

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

  • "Інжиніринг функції сортування" Дж. Л. Бентлі та доктора Макілроя, опублікованого в "Практиці та досвіді програмного забезпечення", т. 23 (11), листопад 1993 р., Йде на широке обговорення питань, і вони обрали алгоритм адаптивного розподілу, що базується частково. розмір набору даних. Існує багато обговорень компромісів для різних підходів.

  • Пошук в Google "медіани-трьох" працює досить добре для подальшого відстеження.

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


4
Медіана 3 - НЕ перша остання середина. Виберіть три випадкові індекси та візьміть середнє значення цього. Вся справа в тому, щоб переконатися, що ваш вибір стрижнів не є детермінованим - якщо це так, найгірші дані можуть бути досить генеровані.
mindvirus

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

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

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 кожен крок, що веде до журналу (n) терміну складності;
Кріс

11

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

Кращою технікою є метод медіани-3, де ви вибираєте три елементи навмання і вибираєте середину. Ви знаєте, що обраний вами елемент не буде першим чи останнім, але також, за теоремою про центральну межу, розподіл середнього елемента буде нормальним, це означає, що ви будете прагнути до середини (а значить , n lg n час).

Якщо ви абсолютно хочете гарантувати O (nlgn) час виконання алгоритму, метод стовпців-5 для знаходження медіани масиву працює в O (n) час, а це означає, що рівняння повторення для quicksort в гіршому випадку буде бути 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 (n)).


0

В середньому, середня 3 хороша для малих n. Медіана 5 є трохи кращою для більших n. Ще більш краща ninther, яка є "медіаною трьох медіанів з трьох", дуже велика 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.