Quicksort медленнее, чем Mergesort?

Вчера я работал над реализацией быстрой сортировки, а затем запустил ее, ожидая более быстрого выполнения, чем сортировка слиянием (которую я также реализовал). Я выполнил оба, и хотя быстрая сортировка была быстрее для небольших наборов данных - 100 элементов (и я действительно убедился, что она работает), сортировка слиянием довольно быстро стала более быстрым алгоритмом. Меня учили, что быстрая сортировка почти всегда «быстрее», чем сортировка слиянием, и я понимаю, что по этой теме ведутся споры, но я, по крайней мере, ожидал, что она будет ближе, чем эта. Для наборов данных> 10000 элементов сортировка слиянием была более чем в 4 раза быстрее. Этого следует ожидать, или в моем коде быстрой сортировки есть ошибка?

Сортировка слиянием:

public static void mergeSort(int[ ] e)
{
    if (e.length <= 1) return;
    int[] first = new int[e.length/2];
    int[] second = new int[e.length - first.length];
    System.arraycopy(e, 0, first, 0, first.length);
    System.arraycopy(e, first.length, second, 0, second.length);
    mergeSort(first);
    mergeSort(second);
    System.arraycopy(merge(first, second), 0, e, 0, e.length);
}

private static int[] merge(int[] first, int[] second) {
    int iFirst = 0;
    int iSecond = 0;
    int iCombined = 0;

    int[] combined = new int[first.length + second.length];
    while(iFirst < first.length && iSecond < second.length) {
        if (first[iFirst] > second[iSecond]) {
            combined[iCombined++] = second[iSecond++];
        }
        else combined[iCombined++] = first[iFirst++];
    }
    for(; iFirst < first.length; iFirst++) {
        combined[iCombined++] = first[iFirst];
    }
    for(; iSecond < second.length; iSecond++) {
        combined[iCombined++] = second[iSecond];
    }
    return combined;
}

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

public static void quicksort(int[] a, int first, int last) {
    if (first >= last) return;

    int partitionIndex = partition(a, first, last);
    quicksort(a, first, partitionIndex - 1);
    quicksort(a, partitionIndex + 1, last);
}

public static int partition(int[] x, int first, int last) {
    int left = first;
    int right = last;
    int pivot = x[first];
    int pivotIdx = first;

    while(left <= right) {
        while(left < x.length && x[left] <= pivot) left++;
        while(right >= 0 && x[right] > pivot) right--;
        if (left <= right) {
            int temp = x[left];
            x[left] = x[right];
            x[right] = temp;
        }
    }
    pivotIdx = right;
    x[first] = x[right];
    x[pivotIdx] = pivot;
    return pivotIdx;
}

person John Murano    schedule 31.01.2009    source источник


Ответы (15)


На самом деле я просто написал «демонстрационную программу сравнительной сортировки связанных списков» на C и пришел к аналогичному выводу (что сортировка слиянием превосходит быструю сортировку в большинстве случаев), хотя мне сказали, что быстрая сортировка в любом случае не используется для связанных списков. Я хотел бы отметить, что выбор значений поворота является фактором чудовищ - в моей первоначальной версии в качестве точки поворота использовался случайный узел, а когда я немного его уточнил, чтобы взять среднее значение двух (случайных) узлов время выполнения для 1000000 записей увеличилось с более 4 минут до менее 10 секунд, что сопоставимо с сортировкой слиянием.

Сортировка слиянием и быстрая сортировка имеют один и тот же лучший случай большого O (n * log (n)), и, несмотря на то, что люди могут пытаться утверждать, большое O действительно касается количества итераций, а не количества сравнений. Самая большая разница, которая может быть получена между ними двумя, всегда будет в ущерб быстрой сортировке, и она включает списки, которые уже в значительной степени отсортированы или содержат большое количество связей (когда быстрая сортировка работает лучше, чем сортировка слиянием, разница будет не такой уж большой). Это связано с тем, что связи или уже отсортированные сегменты оптимизируют прямую сортировку слиянием; когда два разделенных списка возвращаются для объединения, если один список уже содержит все меньшие значения, все значения слева будут сравниваться по одному с первым элементом справа, а затем (поскольку возвращенные списки имеют внутренний порядок) дальнейших сравнений не требуется, и правая часть просто повторяется в конце. То есть количество итераций останется постоянным, но количество сравнений сократится вдвое. Если вы говорите о реальном времени и сортируете строки, это дорогое сравнение.

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

Если вам интересно, демонстрационная программа выдает следующий результат:

[root~/C] ./a.out -1 3 
Using "", 0 records
Primary Criteria offset=128

Command (h for help, Q to quit): N
How many records? 4000000
New list is 562500.00 kb

Command (h for help, Q to quit): m

Mergesorting..............3999999 function calls
123539969 Iterations     Comparison calls: 82696100
Elapsed time: 0 min 9 sec


Command (h for help, Q to quit): S
Shuffled.

Command (h for help, Q to quit): q

Quicksorting..............4000000 function calls
190179315 Iterations     Comparison calls: 100817020
Elapsed time: 0 min 23 sec

Альтон без сумасбродных цветов. Я написал еще кое-что об этом примерно на полпути к этой странице. страница.

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

person mk27    schedule 31.01.2009
comment
Это неуместный ответ, так как он использует хранилище связанных списков. - person Stephan Eggermont; 02.08.2011
comment
Вы сказали, что Mergesort и quicksort имеют один и тот же лучший вариант большого O (n * log (n)), но я хочу упомянуть, что Big O предназначен исключительно для верхней границы времени работы (это только в худшем случае) Big Omega описывает нижнюю границу (лучший случай) - person talloaktrees; 22.07.2013

Сортировка слиянием намного медленнее для данных на основе случайных массивов, если они умещаются в оперативной памяти. Я впервые вижу, как это обсуждают.

  • qsort сначала самый короткий подмассив.
  • переключиться на сортировку вставкой ниже 5-25 элементов
  • сделать обычный выбор поворота

Ваш qsort очень медленный, потому что он пытается разбить и qsort массивы длиной 2 и 3.

person Stephan Eggermont    schedule 10.12.2009
comment
+1 Для переключения на сортировку вставкой должно дать хорошее улучшение - person helpermethod; 24.01.2010
comment
Есть ли причина, по которой вы предлагаете оптимизировать реализацию быстрой сортировки, а не реализацию сортировки слиянием? Сортировка слиянием также может выиграть от переключения на сортировку вставкой (см. Пример сортировки по времени). Между прочим, многие реализации языков программирования используют оптимизированную версию сортировки слиянием внутри: Java, Python, C с GNU libc ... Более поздняя версия даже вызывает быструю сортировку более медленного алгоритма. - person Erwan Legrand; 18.03.2014

Ранее обсуждалось на SO: «Почему быстрая сортировка лучше сортировки слиянием?»

~

person yawmark    schedule 31.01.2009

Одно из преимуществ быстрой сортировки для массивов относительно небольшого размера - это просто артефакт аппаратной реализации.

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

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

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

person fastcall    schedule 31.01.2009

На основе этой статьи этой википедии ожидаются ваши результаты.

person JoshBerke    schedule 31.01.2009
comment
@ Стефан Эггермонт: Можете ли вы указать на ошибки в реализации Джона? - person Giorgio; 13.11.2012

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

person Ray Hidayat    schedule 31.01.2009
comment
Я не понимаю аргументацию. Если для быстрой сортировки установлено значение O (n log (n)) в среднем, то это потому, что существуют случаи ниже среднего, и вы не можете их избежать, независимо от того, как вы выбираете опорную точку. Или я что-то не замечаю? - person Giorgio; 11.06.2012

Я мог себе представить, что, напрямую обращаясь к памяти, используя, например, C, можно улучшить производительность Quicksort больше, чем это возможно с Mergesort.

Другая причина в том, что Mergesort требует больше памяти, потому что его сложно реализовать как сортировку на месте.

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

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

person Georg Schölly    schedule 31.01.2009

(1) Существует алгоритм qsort, используемый C qsort (), который не требует дополнительной памяти. Скорее всего, это было изобретено Хоаром. Это ускоряет работу qsort () в C.

(2) Рандомизация данных перед запуском qsort почти всегда ускоряет его.

(3) выбор медианных данных для разворота может ускорить процесс,

person vrdhn    schedule 31.01.2009
comment
Даже если это называется qsort (), это, вероятно, не чистая быстрая сортировка. - person Giorgio; 15.05.2012

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

Quicksort - лучший алгоритм для общего случая, потому что у него низкие накладные расходы, поэтому он имеет хорошую скорость для значений n примерно до 10000 или около того и все еще хорошее время выполнения для произвольно астрономических значений n. У сортировки слиянием есть досадные накладные расходы на запись кадра стека, необходимого для каждого рекурсивного вызова. Таким образом, для низких значений n он имеет ужасающе высокое значение c в RT = cnlogn и не является предпочтительным общим методом сортировки.

Изменить: Software Monkey указала на противоречие: Quicksort усредняет O (nlogn) для случайного ввода, но в худшем случае O (n ^ 2). Таким образом, он на самом деле в некоторой степени ограничен энтропией ваших данных - или вы можете выбрать точку поворота случайным образом. Хотя я все еще могу быть немного не в себе.

person Overflown    schedule 31.01.2009
comment
Быстрая сортировка не может быть одновременно средним случаем O (nlogn) и средним ... между O (nlogn) и O (n ^ 2). - person Lawrence Dol; 31.01.2009
comment
извините, среднее значение O (nlogn) для случайного ввода, но в худшем случае O (n ^ 2) Так что на самом деле это несколько связано с энтропией - person Overflown; 31.01.2009

Если вы реализуете сортировку по куче в качестве базового алгоритма сортировки в худшем случае быстрой сортировки, вы получите алгоритм тета (n log n).

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

Сортировка слиянием

person Yngve Sneen Lindal    schedule 31.01.2009

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

Одна из наиболее широко используемых реализаций qsort (), glibc qsort (), внутренне использует сортировку слиянием в большинстве случаев, когда данные умещаются в памяти. Эта сортировка слиянием выделяет временное пространство памяти, используемое для слияния, что добавляет некоторые накладные расходы на память, но в большинстве случаев она превосходит собственную внутреннюю реализацию быстрой сортировки с хорошим выбором и оптимизацией поворота. glibc использует быструю сортировку только тогда, когда данные и временная память для сортировки слиянием не помещаются в памяти.

Я измерил производительность этих двух реализаций на своей машине с процессором 2,1 ГГц и несколькими ГБ ОЗУ. Входные данные генерируются псевдослучайным генератором, и каждый ключ представляет собой 32-битное целое число без знака, что означает немного больше циклов сравнения, чем целочисленное сравнение из-за интерфейса функции сравнения.

Для сортировки слиянием:

2 MB, time_diff 165.156000 ms, 78.752518 ns per byte
4 MB, time_diff 344.298000 ms, 82.087040 ns per byte
8 MB, time_diff 730.926000 ms, 87.133169 ns per byte
16 MB, time_diff 1541.215000 ms, 91.863573 ns per byte
32 MB, time_diff 3088.924000 ms, 92.057109 ns per byte
64 MB, time_diff 6262.868000 ms, 93.324006 ns per byte
128 MB, time_diff 12887.018000 ms, 96.015766 ns per byte
256 MB, time_diff 26731.597000 ms, 99.582959 ns per byte

Для быстрой сортировки:

2 MB, time_diff 243.519000 ms, 116.118908 ns per byte
4 MB, time_diff 504.975000 ms, 120.395422 ns per byte
8 MB, time_diff 1075.276000 ms, 128.182888 ns per byte
16 MB, time_diff 2183.865000 ms, 130.168498 ns per byte
32 MB, time_diff 4343.993000 ms, 129.461080 ns per byte
64 MB, time_diff 8714.166000 ms, 129.851192 ns per byte
128 MB, time_diff 17881.344000 ms, 133.226395 ns per byte
256 MB, time_diff 36751.029000 ms, 136.908252 ns per byte

Вы можете видеть, что существуют явные различия в производительности между этими двумя реализациями и почему сортировка слиянием предпочтительнее быстрой сортировки в такой широко используемой реализации qsort. Основная причина этой разницы заключается в том, что быстрая сортировка имеет на 10-20% больше сравнений, чем сортировка слиянием, из-за неравномерного разделения на каждом шаге.

person Sangman Kim    schedule 21.11.2012

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

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

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

person Giorgio    schedule 15.05.2012

Были ли ваши наборы данных случайными? Они были частично отсортированы?

Это может повлиять на скорость сортировки ...

Как и в случае с partition () QuickSort, вы должны пропустить, если числа расположены в отсортированном порядке, пока не найдете тот, который не соответствует действительности.

person Calyth    schedule 31.01.2009

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

person DShook    schedule 31.01.2009

Для хорошей производительности быстрой сортировки важно не возвращаться к спискам длиной 1

Вы должны рассматривать сортировку списков из 2, 3 и даже 4 как вложенных ifs, заменяющих при необходимости. Сообщите нам, как изменится производительность.

person Thorbjørn Ravn Andersen    schedule 31.01.2009