QuickSort против MergeSort, что я делаю не так?

Я пытаюсь реализовать несколько алгоритмов сортировки на Java, чтобы сравнить производительность. Из того, что я прочитал, я ожидал, что quickSort будет быстрее, чем mergeSort, но в моем коде это не так, поэтому я предполагаю, что с моим алгоритмом quickSort должна быть проблема:

public class quickSortExample{
public static void main(String[] args){
    Random gen = new Random();
    int n = 1000000;
    int max = 1500000;
    ArrayList<Integer> d = new ArrayList<Integer>();
    for(int i = 0; i < n; i++){
        d.add(gen.nextInt(max));
    }
    ArrayList<Integer> r;
    long start, end;

    start = System.currentTimeMillis();
    r = quickSort(d);
    end = System.currentTimeMillis();
    System.out.println("QuickSort:");
    System.out.println("Time: " + (end-start));
    //System.out.println(display(d));
    //System.out.println(display(r));
}

public static ArrayList<Integer> quickSort(ArrayList<Integer> data){
    if(data.size() > 1){
        int pivotIndex = getPivotIndex(data);
        int pivot = data.get(pivotIndex);
        data.remove(pivotIndex);
        ArrayList<Integer> smallers = new ArrayList<Integer>();
        ArrayList<Integer> largers = new ArrayList<Integer>();
        for(int i = 0; i < data.size(); i++){
            if(data.get(i) <= pivot){
                smallers.add(data.get(i));
            }else{
                largers.add(data.get(i));
            }
        }
        smallers = quickSort(smallers);
        largers = quickSort(largers);
        return concat(smallers, pivot, largers);
    }else{
        return data;
    }
}

public static int getPivotIndex(ArrayList<Integer> d){
    return (int)Math.floor(d.size()/2.0);
}

public static ArrayList<Integer> concat(ArrayList<Integer> s, int p, ArrayList<Integer> l){
    ArrayList<Integer> arr = new ArrayList<Integer>(s);
    arr.add(p);
    arr.addAll(l);

    return arr;
}

public static String display(ArrayList<Integer> data){
    String s = "[";
    for(int i=0; i < data.size(); i++){
        s += data.get(i) + ", ";
    }
    return (s+"]");
}

}

Результаты (для 1 миллиона целых чисел от 0 до 1500000):

mergeSort (также реализовано с arrayList): 1,3 секунды (в среднем) (0,7 секунды с int [] вместо этого)

quickSort: 3 секунды (в среднем)

Это просто плохой выбор моей опорной точки, или в алгоритме тоже есть недоработки.

Кроме того, есть ли более быстрый способ закодировать его с помощью int [] вместо ArrayList ()? (Как вы объявляете размер массива для массивов большего / меньшего размера?)

PS: Теперь можно реализовать его на месте, чтобы он использовал меньше памяти, но не в этом суть.

РЕДАКТИРОВАТЬ 1: я заработал 1 секунду, изменив метод concat. Спасибо!


person nbarraille    schedule 27.01.2011    source источник
comment
Первый вопрос: действительно ли они оба работают?   -  person Oliver Charlesworth    schedule 27.01.2011


Ответы (6)


PS: Теперь можно реализовать его на месте, чтобы он использовал меньше памяти, но не в этом суть.

Дело не только в том, чтобы использовать меньше памяти. Вся эта дополнительная работа, которую вы выполняете в подпрограмме «concat» вместо правильной быстрой сортировки на месте, почти наверняка стоит так дорого. Если вы все равно можете использовать дополнительное пространство, вам всегда следует кодировать сортировку слиянием, потому что она, как правило, выполняет меньше сравнений, чем QuickSort.

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

person Pointy    schedule 27.01.2011

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

Двумя основными виновниками являются smallers и largers. Размер по умолчанию для ArrayList - 10. При первоначальном вызове quickSort хороший поворот будет означать, что более мелкие и большие вырастут до 500 000. Поскольку размер ArrayList увеличивается только вдвое, когда он достигает своей емкости, его придется изменить примерно в 19 раз.

Поскольку вы делаете новый меньше и больше с каждым уровнем рекурсии, вы будете выполнять примерно 2 * (19 + 18 + ... + 2 + 1) изменения размера. Это около 400 изменений размера, которые должны выполнить объекты ArrayList, прежде чем они будут объединены. В процессе конкатенации, вероятно, будет выполнено такое же количество изменений размера.

В общем, это очень много лишней работы.

Ой, только что заметил data.remove(pivotIndex). Выбранный индекс сводной таблицы (середина массива) также будет вызывать дополнительные операции с памятью (хотя середина обычно является лучшим выбором, чем начало или конец или массив). Arraylist скопирует весь блок памяти «вправо» от точки поворота на один шаг влево в массиве поддержки.

Небольшое примечание о выбранной опорной точке, поскольку целые числа, которые вы сортируете, равномерно распределены между n и 0 (если Random соответствует своему названию), вы можете использовать это для выбора хороших опорных точек. То есть первый уровень быстрой сортировки должен выбрать max * 0,5 в качестве точки поворота. Второй уровень с меньшими должен выбрать максимум * 0,25, а второй уровень с более крупными должен выбрать максимум * 0,75 (и так далее).

person Dunes    schedule 27.01.2011

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

template<class T> void quickSortR(T* a, long N) {

  long i = 0, j = N;        
  T temp, p;

  p = a[ N/2 ];     


  do {
    while ( a[i] < p ) i++;
    while ( a[j] > p ) j--;

    if (i <= j) {
      temp = a[i]; a[i] = a[j]; a[j] = temp;
      i++; j--;
    }
  } while ( i<=j );



  if ( j > 0 ) quickSortR(a, j);
  if ( N > i ) quickSortR(a+i, N-i);
}
person Max    schedule 27.01.2011

Основы ООП и структур данных в Java Ричард Винер, Льюис Дж. Пинсон перечисляют быструю сортировку следующим образом, которая может быть или не быть быстрее (я подозреваю, что это так), чем ваша реализация. .

public static void quickSort (Comparable[] data, int low, int high) {
    int partitionIndex;
    if (high - low > 0) {
        partitionIndex = partition(data, low, high);
        quickSort(data, low, partitionIndex - 1);
        quickSort(data, partitionIndex + 1, high);
    }
}

private static int partition (Comparable[] data, int low, int high) {
    int k, j;
    Comparable temp, p;
    p = data[low]; // Partition element
    // Find partition index(j).
    k = low;
    j = high + 1;

    do {
        k++;
    } while (data[k].compareTo(p) <= 0 && k < high);

    do {
        j--;
    } while (data[j].compareTo(p) > 0);

    while (k < j) {
        temp = data[k];
        data[k] = data[j];
        data[j] = temp;

        do {
            k++;
        } while (data[k].compareTo(p) <= 0);

        do {
            j--;
        } while (data[j].compareTo(p) > 0);
    }
    // Move partition element(p) to partition index(j).
    if (low != j) {
        temp = data[low];
        data[low] = data[j];
        data[j] = temp;
    }
    return j; // Partition index
}
person Robert    schedule 27.01.2011

Согласен, причина в ненужном копировании. Далее следуют еще несколько примечаний.

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

(int)Math.floor(d.size()/2.0) эквивалентно d.size()/2.

data.remove(pivotIndex); - ненужное копирование n/2 элементов. Вместо этого вы должны проверить в следующем цикле, i == pivotIndex, и пропустить этот элемент. (Что ж, вам действительно нужно выполнить сортировку на месте, но я просто предлагаю простые улучшения.)

Помещать все элементы, которые равны pivot, в одну («меньшую») часть - плохая идея. Представьте, что происходит, когда все элементы массива равны. (Опять же, в данном случае это не проблема.)


for(i = 0; i < s.size(); i++){
    arr.add(s.get(i));
}

эквивалентно arr.addAll(s). И, конечно же, здесь снова ненужное копирование. Вы можете просто добавить все элементы из правой части в левую вместо создания нового списка.

(Как вы объявляете размер массива для массивов большего / меньшего размера?)

Я не уверен, правильно ли я понял, но ты хочешь array.length?

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

person adamax    schedule 27.01.2011

Технически Mergesort имеет лучшее поведение во времени (Θ (nlogn) наихудший и средний случаи), чем Quicksort (Θ (n ^ 2) наихудший случай, Θ ( nlogn) средний случай). Так что вполне возможно найти исходные данные, по которым Mergesort превосходит Quicksort. В зависимости от того, как вы выбираете опорные точки, вы можете сделать худший случай редким. Но для простой версии Quicksort в «худшем случае» будут отсортированные (или почти отсортированные) данные, которые могут быть довольно обычным вводом.

Вот что Википедия говорит об этих двух:

На типичных современных архитектурах эффективные реализации быстрой сортировки обычно превосходят сортировку слиянием при сортировке массивов на основе ОЗУ. С другой стороны, сортировка слиянием - стабильная сортировка, лучше распараллеливается и более эффективна при обработке последовательных носителей с медленным доступом. [Необходима цитата] Сортировка слиянием часто является лучшим выбором для сортировки связанного списка: в этой ситуации она относительно легко реализовать сортировку слиянием таким образом, что для этого требуется только Θ (1) дополнительного места, а медленная производительность произвольного доступа связанного списка приводит к тому, что некоторые другие алгоритмы (например, быстрая сортировка) работают плохо, а другие (например, как heapsort) совершенно невозможно.

person T.E.D.    schedule 27.01.2011