对快速排序和合并排序进行基准测试得出合并排序更快
Benchmarking quicksort and mergesort yields that mergesort is faster
我已经尝试过基准测试,出于某种原因,当在 1M 元素的数组上尝试它们时,Mergesort
在 0.3 秒内对它进行了排序,而 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++;
}
}
快速排序:
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;
}
你的实现有点简单:
mergesort
在每次递归调用时分配 2 个新数组,这是昂贵的,但一些 JVM 在优化此类编码模式方面出奇地高效。
quickSort
使用了一个糟糕的枢轴选择,即子数组的最后一个元素,这为排序的子数组提供了二次时间,包括那些具有相同元素的子数组。
数据集,一个小区间伪随机数的数组0..127
,导致quickSort
实现的缺点比mergesort
实现的低效率差很多] 版本。增加数据集大小应该会使这一点更加明显,甚至可能由于太多的递归调用而导致 堆栈溢出。具有相同值、增加或减少集合以及此类序列的组合等常见模式的数据集将导致 quickSort
实现的灾难性性能。
这里是一个稍微修改过的版本,它对枢轴(数组 3/4 处的元素)的病态选择较少,并且有一个循环来检测枢轴值的重复项,以提高具有重复值的数据集的效率。它在我的标准排序基准测试中的表现要好得多(100 倍),数组只有 40k 个元素,但仍然比 radixsort 慢得多(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;
}
对于 OP 的数据集,假设分布具有适当的随机性,扫描重复项有助于提高性能。正如预期的那样,选择不同的枢轴,无论是第一个、最后一个、中间、3/4 或 2/3 甚至中位数 3 几乎没有影响。
对其他非随机分布的进一步测试表明,由于选择了枢轴,此 quickSort
实施的性能非常糟糕。在我的基准测试中,通过选择数组的 3/4 或 2/3 处的元素作为枢轴,可以获得很大的性能改进(50k 样本提高 300 倍,比标准合并排序快 40%,时间与 radix_sort
相当) .
- Mergesort 具有对所有分布稳定和可预测的明显优势,但它需要数据集大小的 50% 到 100% 之间的额外内存。
- 精心实施的快速排序在许多情况下速度稍快,并且就地执行,递归只需要 log(N) 个堆栈 space。然而,它并不稳定,量身定制的发行版将表现出灾难性的性能,可能会崩溃。
- 基数排序仅适用于特定类型的数据,例如整数和固定长度的字符串。它还需要额外的内存。
- Countingsort 对于 OP 的数据集来说是最有效的,因为它只需要一个包含 128 个整数的数组来计算不同值的出现次数,已知值在 0..127 范围内。对于任何分布,它都将在线性时间内执行。
我已经尝试过基准测试,出于某种原因,当在 1M 元素的数组上尝试它们时,Mergesort
在 0.3 秒内对它进行了排序,而 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++;
}
}
快速排序:
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;
}
你的实现有点简单:
mergesort
在每次递归调用时分配 2 个新数组,这是昂贵的,但一些 JVM 在优化此类编码模式方面出奇地高效。quickSort
使用了一个糟糕的枢轴选择,即子数组的最后一个元素,这为排序的子数组提供了二次时间,包括那些具有相同元素的子数组。
数据集,一个小区间伪随机数的数组0..127
,导致quickSort
实现的缺点比mergesort
实现的低效率差很多] 版本。增加数据集大小应该会使这一点更加明显,甚至可能由于太多的递归调用而导致 堆栈溢出。具有相同值、增加或减少集合以及此类序列的组合等常见模式的数据集将导致 quickSort
实现的灾难性性能。
这里是一个稍微修改过的版本,它对枢轴(数组 3/4 处的元素)的病态选择较少,并且有一个循环来检测枢轴值的重复项,以提高具有重复值的数据集的效率。它在我的标准排序基准测试中的表现要好得多(100 倍),数组只有 40k 个元素,但仍然比 radixsort 慢得多(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;
}
对于 OP 的数据集,假设分布具有适当的随机性,扫描重复项有助于提高性能。正如预期的那样,选择不同的枢轴,无论是第一个、最后一个、中间、3/4 或 2/3 甚至中位数 3 几乎没有影响。
对其他非随机分布的进一步测试表明,由于选择了枢轴,此 quickSort
实施的性能非常糟糕。在我的基准测试中,通过选择数组的 3/4 或 2/3 处的元素作为枢轴,可以获得很大的性能改进(50k 样本提高 300 倍,比标准合并排序快 40%,时间与 radix_sort
相当) .
- Mergesort 具有对所有分布稳定和可预测的明显优势,但它需要数据集大小的 50% 到 100% 之间的额外内存。
- 精心实施的快速排序在许多情况下速度稍快,并且就地执行,递归只需要 log(N) 个堆栈 space。然而,它并不稳定,量身定制的发行版将表现出灾难性的性能,可能会崩溃。
- 基数排序仅适用于特定类型的数据,例如整数和固定长度的字符串。它还需要额外的内存。
- Countingsort 对于 OP 的数据集来说是最有效的,因为它只需要一个包含 128 个整数的数组来计算不同值的出现次数,已知值在 0..127 范围内。对于任何分布,它都将在线性时间内执行。