基准化快速排序和合并排序产生的合并排序速度更快

问题描述 投票:2回答:1

[我已经尝试进行基准测试,并且出于某种原因,当在100万个元素数组上尝试将它们都使用时,Mergesort将其排序为0.3s,Quicksort则为1.3s。

我听说快速排序通常由于其内存管理而更快,但是如何解释这些结果呢?

我正在运行MacBook Pro,如果有任何区别。输入是一组从0到127的随机生成的整数。

代码使用Java:

MergeSort:

static void mergesort(int arr[]) {
    int n = arr.length;
    if (n < 2)
        return;
    int mid = n / 2;
    int left[] = new int[mid];
    int right[] = new int[n - mid];
    for (int i = 0; i < mid; i++)
        left[i] = arr[i];
    for (int i = mid; i < n; i++)
        right[i - mid] = arr[i];
    mergesort(left);
    mergesort(right);
    merge(arr, left, right);
}

public static void merge(int arr[], int left[], int right[]) {
    int nL = left.length;
    int nR = right.length;
    int i = 0, j = 0, k = 0;
    while (i < nL && j < nR) {
        if (left[i] <= right[j]) {
            arr[k] = left[i];
            i++;
        } else {
            arr[k] = right[j];
            j++;
        }
        k++;
    }
    while (i < nL) {
        arr[k] = left[i];
        i++;
        k++;
    }
    while (j < nR) {
        arr[k] = right[j];
        j++;
        k++;
    }
}

快速排序:

public static void quickSort(int[] arr, int start, int end) {
    int partition = partition(arr, start, end);

    if (partition - 1 > start) {
        quickSort(arr, start, partition - 1);
    }
    if (partition + 1 < end) {
        quickSort(arr, partition + 1, end);
    }
}

public static int partition(int[] arr, int start, int end) {
    int pivot = arr[end];

    for (int i = start; i < end; i++) {
        if (arr[i] < pivot) {
            int temp = arr[start];
            arr[start] = arr[i];
            arr[i] = temp;
            start++;
        }
    }

    int temp = arr[start];
    arr[start] = pivot;
    arr[end] = temp;

    return start;
}
java benchmarking quicksort mergesort
1个回答
3
投票

您的实现有点简单:

  • [mergesort在每个递归调用中分配2个新数组,这很昂贵,但是某些JVM在优化此类编码模式方面出奇地有效。
  • [quickSort使用错误的支点选择,即子数组的最后一个元素,这给排序后的子数组(包括那些具有相同元素的子数组)提供了二次时间。

数据集是一个伪随机数在0..127较小范围内的数组,导致quickSort实现的缺点比mergesort版本的效率低得多。增大数据集的大小应使其更加明显,甚至可能由于太多的递归调用而导致stackoverflow。具有通用模式(例如相同的值,增加或减少的集合以及此类序列的组合)的数据集将导致quickSort实现的灾难性性能。

这里是经过稍微修改的版本,对枢轴(数组3/4处的元素)的病理选择较少,并且有一个循环可检测枢轴值的重复项,从而提高具有重复值的数据集的效率。在仅包含4万个元素的数组的标准排序基准上,它的性能要好得多(100x),但比radixsort慢得多(8x):

public static void quickSort(int[] arr, int start, int end) {
    int p1 = partition(arr, start, end);
    int p2 = p1;

    /* skip elements identical to the pivot */
    while (++p2 <= end && arr[p2] == arr[p1])
        continue;

    if (p1 - 1 > start) {
        quickSort(arr, start, p1 - 1);
    }
    if (p2 < end) {
        quickSort(arr, p2, end);
    }
}

public static int partition(int[] arr, int start, int end) {
    /* choose pivot at 3/4 or the array */
    int i = end - ((end - start + 1) >> 2);
    int pivot = arr[i];
    arr[i] = arr[end];
    arr[end] = pivot;

    for (i = start; i < end; i++) {
        if (arr[i] < pivot) {
            int temp = arr[start];
            arr[start] = arr[i];
            arr[i] = temp;
            start++;
        }
    }

    int temp = arr[start];
    arr[start] = pivot;
    arr[end] = temp;

    return start;
}

对于OP的数据集,假设分布具有良好的随机性,则扫描重复项可改善性能。如预期的那样,选择不同的枢轴,无论是第一个,最后一个,中间,3/4或2/3或什至3的中间值都几乎没有影响。

对其他非随机分布的进一步测试显示,由于选择了枢轴,因此此quickSort实现的灾难性性能。根据我的基准,通过选择在数组的3/4或2/3处旋转元素,可以大大提高性能(对于50k样本,其性能提高了300倍,比标准合并排序快40%,与radix_sort相当的时间)。 >

  • Mergesort具有对所有分布稳定且可预测的显着优势,但它需要额外的存储空间,其大小介于数据集大小的50%到100%之间。
  • 经过仔细实施的Quicksort在许多情况下会更快一些,并且可以就地执行,只需要log(N)堆栈空间即可递归。但是它并不稳定,量身定制的发行版将表现出灾难性的性能,甚至可能崩溃。
  • Radixsort仅适用于特定类型的数据,例如整数和固定长度的字符串。它还需要额外的内存。
  • Countingsort对于OP的数据集将是最有效的,因为它只需要一个128个整数数组即可计算不同值的出现次数,已知范围为0..127。对于任何分布,它将在线性时间内执行。
© www.soinside.com 2019 - 2024. All rights reserved.