【问题标题】:QuickSort vs MergeSort, what am I doing wrong?QuickSort 和 MergeSort,我做错了什么?
【发布时间】:2011-01-27 16:43:19
【问题描述】:

我正在尝试在 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 之间的 100 万个整数):

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

快速排序:3 秒(平均)

仅仅是我的支点选择不好,还是算法中也存在一些缺陷。

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

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

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

【问题讨论】:

  • 第一个问题:它们都有效吗?

标签: java performance sorting quicksort mergesort


【解决方案1】:

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

这不仅仅是为了使用更少的内存。您在“concat”例程中所做的所有额外工作,而不是进行适当的就地快速排序,几乎可以肯定是成本如此之高。如果您仍然可以使用额外的空间,则应该始终编写合并排序,因为它进行的比较往往比快速排序要少。

想一想:在“concat()”中,您不可避免地必须再次遍历子列表,进行更多比较。如果您在一个数组中就地进行交换,那么一旦您决定交换两个位置,您就不会再次做出决定。

【讨论】:

    【解决方案2】:

    正如你所说,我认为快速排序的主要问题是它没有到​​位。

    两个主要罪魁祸首是smallerslargers。 ArrayList 的默认大小是 10。在对 quickSort 的初始调用中,一个好的主元将意味着较小的和较大的增长到 500,000。由于 ArrayList 在达到容量时只会增加一倍的大小,因此必须将其大小调整为大约 19 倍。

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

    总而言之,这是很多额外的工作。

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

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

    【讨论】:

      【解决方案3】:

      我认为,您的算法效率很低,因为您使用的是中间数组 = 更多内存 + 更多时间用于分配/复制。这是 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);
      }
      

      【讨论】:

        【解决方案4】:

        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
        }
        

        【讨论】:

          【解决方案5】:

          我同意原因是不必要的复制。后面还有一些注释。

          枢轴索引的选择不好,但这不是问题,因为您的数字是随机的。

          (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吗?

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

          【讨论】:

            【解决方案6】:

            从技术上讲,Mergesort 的时间行为(Θ(nlogn) 最坏和平均情况)比快速排序(Θ(n^2) 最坏情况,Θ(nlogn) 平均情况)。因此,很有可能找到 Mergesort 优于 Quicksort 的输入。根据您选择支点的方式,您可以使最坏的情况变得罕见。但对于 Quicksort 的简单版本,“最坏情况”将是已排序(或几乎已排序)的数据,这可能是相当常见的输入。

            Here's what Wikipedia says关于二:

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

            【讨论】:

              猜你喜欢
              • 2020-09-29
              • 2015-07-13
              • 2011-06-01
              • 1970-01-01
              • 2012-02-17
              • 2011-12-22
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多