对快速排序和归并排序进行基准测试得出归并排序更快

Mar*_*nas 2 java benchmarking mergesort quicksort

我已经尝试过基准测试,出于某种原因,当在 1M 元素的数组上尝试它们时,它们在0.3 秒内Mergesort排序并Quicksort花费了 1.3 秒。

我听说快速排序通常更快,因为它的内存管理,但如何解释这些结果?

如果这有什么不同,我正在运行 MacBook Pro。输入是一组从 0 到 127 的随机生成的整数。

代码在Java中:

归并排序:

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++;
    }
}

Run Code Online (Sandbox Code Playgroud)

快速排序:

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;
}
Run Code Online (Sandbox Code Playgroud)

chq*_*lie 6

您的实现有点简单:

  • mergesort 在每次递归调用时分配 2 个新数组,这很昂贵,但一些 JVM 在优化此类编码模式方面非常有效。
  • quickSort 使用了一个糟糕的主元选择,子数组的最后一个元素,它为排序的子数组提供了二次时间,包括那些具有相同元素的子数组。

数据集,一个小范围内的伪随机数数组0..127,导致实现的缺点quickSortmergesort版本的低效性能差很多。增加数据集大小应该会使这一点更加明显,甚至可能由于递归调用过多而导致堆栈溢出。具有共同模式(例如相同值、增加或减少集合以及此类序列的组合)的数据集将导致实现的灾难性性能quickSort

这是一个稍微修改的版本,其中枢轴的病态选择较少(数组的 3/4 处的元素)和用于检测枢轴值重复的循环,以提高具有重复值的数据集的效率。在我的标准排序基准测试中,它的性能要好得多(100 倍),数组只有 40k 个元素,但仍然比基数排序慢得多(8 倍):

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;
}
Run Code Online (Sandbox Code Playgroud)

对于 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 范围内。对于任何分布,它将在线性时间内执行。