QuickSort vs MergeSort,我做错了什么?

时间:2011-01-27 16:43:20

标签: java performance sorting 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+"]");
}

}

结果(0到1500000之间的1百万个整数):

mergeSort(也用arrayList实现):1.3sec(平均值)(0.7秒,而int []代替)

quickSort:3秒(平均值)

这只是我的支点的选择,这是不好的,或者算法中也存在一些缺陷。

另外,有没有更快的方法用int []而不是ArrayList()来编码它? (如何为largers / smallers数组声明数组的大小?)

PS:我现在可以以原位方式实现它,因此它使用更少的内存,但这不是重点。

编辑1:我通过更改concat方法获得1秒。 谢谢!

6 个答案:

答案 0 :(得分:4)

  

PS:我现在可以以原位方式实现它,因此它使用更少的内存,但这不是重点。

这不仅仅是使用更少的内存。你在“concat”例程中所做的所有额外工作,而不是做一个正确的就地QuickSort,几乎可以肯定是这么多的成本。如果你仍然可以使用额外的空间,你应该总是编写一个合并排序,因为它比QuickSort更容易进行比较。

想一想:在“concat()”中,你不可避免地需要再次传递子列表,进行更多的比较。如果您就地进行了交换,所有都在一个阵列中,那么一旦您决定交换两个位置,您就不会再做出决定。

答案 1 :(得分:2)

我认为你的快速排序的主要问题就是它没有到​​位。

两个主要罪魁祸首是smallerslargers。 ArrayList的默认大小为10.在对quickSort的初始调用中,一个好的数据透视表示较小的和较大的数字会增长到500,000。由于ArrayList在达到容量时只会增加一倍,因此必须调整大小约19倍。

由于你在每个递归级别上制作一个越来越小的新东西,你将会执行大约2 *(19 + 18 + ... + 2 + 1)的大小调整。大约400个调整大小,ArrayList对象在连接之前必须执行。连接过程可能会执行相似数量的调整。

总而言之,这是一项额外的工作。

糟糕,刚刚注意到data.remove(pivotIndex)。选择的枢轴索引(数组的中间)也将导致额外的内存操作(即使中间通常是比开始或结束或数组更好的选择)。这就是arraylist会将整个内存块复制到支持数组中左侧枢轴的“右侧”一步。

关于所选枢轴的快速说明,因为您排序的整数均匀分布在n和0之间(如果Random符合其名称),您可以使用它来选择好的枢轴。也就是说,第一级快速排序应选择max * 0.5作为其枢轴。较小的第二级应选择max * 0.25,而使用较大的第二级应选择max * 0.75(依此类推)。

答案 2 :(得分:1)

我认为,你的算法是非常低效的,因为你正在使用中间数组=更多内存+更多时间进行分配/复制。这是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);
}

答案 3 :(得分:1)

Fundamentals of OOP and data structures in Java By Richard Wiener, Lewis J. Pinson列出了以下快速排序,可能会或可能不会比您的实施更快(我怀疑它)...

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
}

答案 4 :(得分:0)

我同意原因是不必要的复制。接下来会有更多笔记。

枢轴索引的选择很糟糕,但这不是问题,因为你的数字是随机的。

(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)。当然,这里还有不必要的复制。您只需将右侧部分中的所有元素添加到左侧,而不是创建新列表。

  

(如何为largers / smallers数组声明数组的大小?)

我不确定我是否帮助你,但是你想要array.length吗?

所以,我认为即使没有实施就地排序,也可以显着提高性能。

答案 5 :(得分:0)

从技术上讲,Mergesort比Quicksort(Θ(n ^ 2)最坏的情况具有更好的时间行为(Θ(nlogn)最差和平均情况)Θ(nlogn)平均情况)。因此,很有可能找到Mergesort优于Quicksort的输入。根据您如何挑选枢轴,您可以使最坏情况变得罕见。但对于Quicksort的简单版本,“最坏情况”将被排序(或几乎排序)数据,这可能是一个相当常见的输入。

Here's what Wikipedia says关于这两个:

  

在典型的现代建筑中,   高效的快速实施   通常优于mergesort   排序基于RAM的数组。在另一   合并排序是一种稳定的排序,   并行化更好,更多   有效处理缓慢访问   顺序媒体。[引证需要]   合并排序通常是最佳选择   用于排序链表:在此   情况相对容易   以这种方式实现合并排序   它只需要Θ(1)额外   空间和慢速随机访问   链表的性能   一些其他算法(如   快速表演,表现不佳等等   (如heapsort)完全   不可能的。