Кто-нибудь видел это улучшение быстрой сортировки раньше? - PullRequest
25 голосов
/ 21 января 2010

Обработка повторяющихся элементов в предыдущих быстрой сортировке

Я нашел способ более эффективно обрабатывать повторяющиеся элементы в быстрой сортировке и хотел бы знать, видел ли кто-нибудь это раньше.

Этот метод значительно снижает накладные расходы, связанные с проверкой повторяющихся элементов, что повышает производительность как с повторяющимися элементами, так и без них. Как правило, повторяющиеся элементы обрабатываются несколькими различными способами, которые я сначала перечислю.

Во-первых, существует метод голландского национального флага, который сортирует массив как [ < pivot | == pivot | unsorted | > pivot].

Во-вторых, существует метод помещения равных элементов в крайнее левое положение во время сортировки, а затем перемещения их в центр, сортировка составляет [ == pivot | < pivot | unsorted | > pivot], а затем после сортировки элементы == перемещаются в центр.

В-третьих, при разбиении Bentley-McIlroy элементы == располагаются с обеих сторон, поэтому сортировка равна [ == pivot | < pivot | unsorted | > pivot | == pivot], а затем элементы == перемещаются в середину.

Последние два метода сделаны в попытке уменьшить накладные расходы.

Мой метод

Теперь позвольте мне объяснить, как мой метод улучшает быструю сортировку за счет уменьшения количества сравнений. Я использую две функции быстрой сортировки вместе, а не одну.

Первую функцию я назову q1, и она сортирует массив как [ < pivot | unsorted | >= pivot].

Вторую функцию я назову q2, и она сортирует массив как [ <= pivot | unsorted | > pivot].

Теперь давайте посмотрим на их использование в тандеме, чтобы улучшить обработку повторяющихся элементов.

Прежде всего, мы вызываем q1, чтобы отсортировать весь массив. Он выбирает пивот, который мы далее будем называть pivot1, а затем сортирует вокруг pivot1. Таким образом, наш массив отсортирован по этой точке как [ < pivot1 | >= pivot1 ].

Затем, для раздела [ < pivot1], мы снова отправляем его в q1, и эта часть является вполне нормальной, поэтому давайте сначала отсортируем другой раздел.

Для раздела [ >= pivot1] мы отправляем его на q2. q2 выбирает сводку, на которую мы будем ссылаться как pivot2 из этого подмассива, и сортирует ее по [ <= pivot2 | > pivot2].

Если мы посмотрим теперь на весь массив, наша сортировка выглядит как [ < pivot1 | >= pivot1 and <= pivot2 | > pivot2]. Это очень похоже на быструю сортировку с двойным поворотом.

Теперь вернемся к подмассиву внутри q2 ([ <= pivot2 | > pivot2]).

Для раздела [ > pivot2] мы просто отправляем его обратно на q1, что не очень интересно.

Для раздела [ <= pivot2] сначала проверяем, если pivot1 == pivot2. Если они равны, то этот раздел уже отсортирован, потому что все они равные элементы! Если стержни не равны, то мы просто отправляем этот раздел на q2 снова, который выбирает свод (далее pivot3), сортирует, и если pivot3 == pivot1, то не нужно сортировать [ <= pivot 3] скоро.

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

Есть еще одно возможное улучшение, которое я еще не пробовал - это проверка в qs2, является ли размер раздела [ <= pivot2] достаточно большим (или раздел [> pivot2] очень маленьким) по сравнению с размер его общего подмассива, а затем выполнить более стандартную проверку для повторяющихся элементов в этом случае (один из методов, перечисленных выше).

Исходный код

Вот две очень упрощенные функции qs1 и qs2. Они используют метод сортировки сходящихся указателей Седжвика. Очевидно, что они могут быть очень оптимизированы (например, они крайне плохо выбирают опорные точки), но это только для того, чтобы показать идею. Моя собственная реализация длиннее, быстрее и намного сложнее для чтения, поэтому давайте начнем с этого:

// qs sorts into [ < p | >= p ]
void qs1(int a[], long left, long right){
    // Pick a pivot and set up some indicies
    int pivot = a[right], temp;
    long i = left - 1, j = right;
    // do the sort
    for(;;){
        while(a[++i] < pivot);
        while(a[--j] >= pivot) if(i == j) break;
        if(i >= j) break;
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
    // Put the pivot in the correct spot
    temp = a[i];
    a[i] = a[right];
    a[right] = temp;

    // send the [ < p ] partition to qs1
    if(left < i - 1)
        qs1(a, left, i - 1);
    // send the [ >= p] partition to qs2
    if( right > i + 1)
        qs2(a, i + 1, right);
}

void qs2(int a[], long left, long right){
    // Pick a pivot and set up some indicies
    int pivot = a[left], temp;
    long i = left, j = right + 1;
    // do the sort
    for(;;){
        while(a[--j] > pivot);
        while(a[++i] <= pivot) if(i == j) break;
        if(i >= j) break;
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
    // Put the pivot in the correct spot
    temp = a[j];
    a[j] = a[left];
    a[left] = temp;

    // Send the [ > p ] partition to qs1
    if( right > j + 1)
        qs1(a, j + 1, right);
    // Here is where we check the pivots.
    // a[left-1] is the other pivot we need to compare with.
    // This handles the repeated elements.
    if(pivot != a[left-1])
            // since the pivots don't match, we pass [ <= p ] on to qs2
        if(left < j - 1)
            qs2(a, left, j - 1);
}

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

qs1(array,0,indexofendofarray);

Некоторые тесты

Если вы хотите знать, насколько это быстро, вот немного данных для начинающих. Здесь используется моя оптимизированная версия, а не та, что приведена выше. Тем не менее, приведенный выше по-прежнему гораздо ближе по времени к быстрой сортировке с двойным поворотом, чем время std::sort.

Для очень случайных данных с 2 000 000 элементов я получаю следующие значения (от сортировки нескольких последовательных наборов данных):

std::sort - 1.609 seconds  
dual-pivot quicksort - 1.25 seconds  
qs1/qs2 - 1.172 seconds

Где std::sort - это сортировка стандартной библиотеки C ++, двойная сводная сортировка - это та, которая была разработана несколько месяцев назад Владимиром Ярославским, а qs1/qs2 - моя реализация быстрой сортировки.

На гораздо меньше случайных данных. с 2 000 000 элементов и сгенерированные с rand() % 1000 (что означает, что каждый элемент имеет примерно 2000 копий) время:

std::sort - 0.468 seconds  
dual-pivot quicksort - 0.438 seconds  
qs1/qs2 - 0.407 seconds

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

Кто-нибудь видел это раньше?

Я знаю, что это длинный вопрос / объяснение, но кто-нибудь из вас видел это улучшение раньше? Если так, то почему он не используется?

Ответы [ 5 ]

7 голосов
/ 09 мая 2012

Владимир Ярославский | 11 сентября 12:35 Замена быстрой сортировки в java.util. Массивы с новой двойной поворотной быстрой сортировкой

Визит http://permalink.gmane.org/gmane.comp.java.openjdk.core-libs.devel/2628

5 голосов
/ 24 мая 2012

Чтобы ответить на ваш вопрос, нет, я не видел такого подхода раньше. Я не собираюсь профилировать ваш код и выполнять другую тяжелую работу, но, возможно, следующие шаги / соображения в формальном представлении вашего алгоритма. В реальном мире алгоритмы сортировки реализованы так:

Хорошая масштабируемость / сложность и Низкие накладные расходы

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

Широкий диапазон входов с хорошей производительностью

Там много данных, которые нужно отсортировать. Кроме того, известно, что приложения генерируют данные в виде шаблонов, поэтому важно обеспечить устойчивость этого вида к низкой производительности при определенных шаблонах. Ваш алгоритм оптимизирован для повторных чисел. Что если все числа будут повторяться, но только один раз (то есть seq 1000> file; seq 1000 >> file; shuf file)? Что делать, если номера уже отсортированы? отсортировано назад? как насчет шаблона 1,2,3,1,2,3,1,2,3,1,2,3? 1,2,3,4,5,6,7,6,5,4,3,2,1? 7,6,5,4,3,2,1,2,3,4,5,6,7? Низкая производительность в одном из этих распространенных сценариев нарушает условия сделки! Перед сравнением с опубликованным алгоритмом общего назначения целесообразно подготовить этот анализ.

Низкий риск патологических показателей

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

Удачи на следующих шагах!

0 голосов
/ 16 мая 2014

никому не нравится ваш алгоритм, но мне нравится. Мне кажется, теперь это хороший способ переделать классическую быструю сортировку безопасен для использования с сильно повторяющимися элементами. Ваши подалгоритмы q1 и q2, как мне кажется, на самом деле ОДНО ЖЕ алгоритм за исключением того, что операторы <и <= поменялись местами, и несколько других вещей, которые, если вы хотел бы позволить вам написать более короткий псевдокод для этого (хотя может быть меньше эффективная). Рекомендую прочитать JL Bentley, MD McIlroy: разработка функции сортировки <br> ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ - ПРАКТИКА И ОПЫТ 23,11 (ноябрь 1993) 1249-1265 электронная доступна здесь http://www.skidmore.edu/~meckmann/2009Spring/cs206/papers/spe862jb.pdf чтобы увидеть тесты, через которые они проводят свою быструю сортировку. Ваша идея может быть лучше и / или лучше, но для этого нужно запустить набор тестов, которые они пробовали, используя некоторые конкретный метод выбора оси. Найти тот, который проходит все свои тесты, не страдая при этом квадратичного времени выполнения. Тогда, если, кроме того, ваш алгоритм будет и быстрее, и лучше, чем их, вы, несомненно, получите достойный вклад.

Мне кажется, что "Tukey Ninther", который они используют для генерации пивота, также может быть использован вами и автоматически затруднит возникновение на практике наихудшего случая квадратичного времени. Я имею в виду, если вы просто используете медиану-3 и попробуйте средний и два конечных элемента массива как ваши три, тогда противник заставит начальное состояние массива увеличиваться, а затем уменьшаться, и тогда вы упадете лицом к лицу с квадратичным временем выполнения при не слишком неправдоподобном вводе. Но с Tukey Ninther на 9 элементах мне довольно сложно построить правдоподобный ввод, который причиняет вам боль при квадратичном времени выполнения.

Другой взгляд и предложение: Подумайте о комбинации q1, разделяющей ваш массив, затем q2, разделяющей правый подмассив, как один алгоритм q12, производящий трехстороннее разбиение массива. Теперь вам нужно рекурсировать на 3 подмассивах (или только на 2, если два стержня оказываются равными). Теперь всегда Рекурсировать в наименьшем из подмассивов, на которых вы собирались рекурсировать, в первую очередь, и самое большое ПОСЛЕДНЕЕ - и не реализуйте это самое большое как рекурсию, а просто оставайтесь в той же самой процедуре и возвращайтесь к вершине с сокращенным окном. Сюда в q12 у вас на 1 меньше рекурсивного вызова, чем было бы, но суть в том, теперь для рекурсивного стека НЕВОЗМОЖНО когда-либо иметь длину больше O (logN). ОК? Это решает другую досадную проблему наихудшего случая, которая может пострадать при быстрой сортировке ваш код все равно немного быстрее.

0 голосов
/ 21 января 2010

std: сортировка не совсем быстрая.

Вот результаты, которые я сравниваю с рандомизированной параллельной нерекурсивной быстрой сортировкой:

pnrqSort (longs): .:. 1 000 000 36 мс (элементов в мс: 27777,8)

.:. 5 000 000 140 мс (элементов в мс: 35714,3)

.:. 10 000 000 296мс (элементов в мс: 33783,8)

.:. 50 000 000 1 с 484 мс (элементов в мс: 33692,7)

.:. 100 000 000 2 с 936 мс (элементов в мс: 34059,9)

.:. 250 000 000 8 с 300 мс (элементов в мс: 30120,5)

.:. 400 000 000 12 с 611 мс (элементов в мс: 31718,3)

.:. 500 000 000 16 с 428 мс (элементов в мс: 30435,8)

станд :: сортировать (лонги) .:. 1 000 000 134 мс (элементов в мс: 7462,69)

.:. 5 000 000 716мс (элементов в мс: 6983,24)

std :: вектор сортировки длинных

1 000 000 511мс (элементов в мс: 1956,95)

2 500 000 943мс (элементов в мс: 2651,11)

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

0 голосов
/ 21 января 2010

Это большое улучшение, и я уверен, что оно было реализовано специально, если вы ожидаете много одинаковых объектов. Существует множество подобных твиков.

Если я правильно понимаю все, что вы написали правильно, причина, по которой он не является "известным", заключается в том, что он улучшает базовую производительность O (n2). Это значит, удвоить количество объектов, в четыре раза больше времени. Ваше улучшение не изменит это, если все объекты не равны.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...